diff --git a/.github/workflows/testing-make.yml b/.github/workflows/testing-make.yml index 8755309d4437..4d4333f176ce 100644 --- a/.github/workflows/testing-make.yml +++ b/.github/workflows/testing-make.yml @@ -72,13 +72,14 @@ jobs: pkg-config \ libpng-dev \ libjpeg-turbo8-dev \ + libopencv-dev \ "llvm-${LLVM_VERSION}-dev" \ "clang-${LLVM_VERSION}" \ "lld-${LLVM_VERSION}" \ "liblld-${LLVM_VERSION}-dev" echo "LLVM_CONFIG=llvm-config-${LLVM_VERSION}" | tee -a "$GITHUB_ENV" elif [ "$RUNNER_OS" = "macOS" ]; then - brew install libjpeg-turbo libpng pkgconf protobuf "llvm@${LLVM_VERSION}" "lld@${LLVM_VERSION}" + brew install libjpeg-turbo libpng pkgconf protobuf opencv "llvm@${LLVM_VERSION}" "lld@${LLVM_VERSION}" echo "LLVM_CONFIG=$(brew --prefix "llvm@${LLVM_VERSION}")/bin/llvm-config" | tee -a "$GITHUB_ENV" fi diff --git a/Makefile b/Makefile index 68fc8180943d..22f84f1486a1 100644 --- a/Makefile +++ b/Makefile @@ -521,6 +521,7 @@ SOURCE_FILES = \ HexagonOffload.cpp \ HexagonOptimize.cpp \ ImageParam.cpp \ + Inductive.cpp \ InferArguments.cpp \ InjectHostDevBufferCopies.cpp \ Inline.cpp \ @@ -724,6 +725,7 @@ HEADER_FILES = \ HexagonOffload.h \ HexagonOptimize.h \ ImageParam.h \ + Inductive.h \ InferArguments.h \ InjectHostDevBufferCopies.h \ Inline.h \ @@ -2217,6 +2219,7 @@ TEST_APPS=\ resize \ resnet_50 \ stencil_chain \ + stereobm \ wavelet TEST_APPS_DEPS=$(TEST_APPS:%=%_test_app) diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index ab7d71214e34..13d81b6ec8b9 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -54,6 +54,7 @@ add_app(hexagon_benchmarks) # add_app(hexagon_dma) # TODO(#5374): missing CMake build add_app(hist) add_app(iir_blur) +add_app(iir_cascade) add_app(interpolate) add_app(lens_blur) add_app(linear_algebra) @@ -67,6 +68,7 @@ add_app(resize) # add_app(resnet_50) # TODO(#5374): missing CMake build # add_app(simd_op_check) # TODO(#5374): missing CMake build add_app(stencil_chain) +add_app(stereobm) add_app(unsharp) add_app(wavelet) # keep-sorted end diff --git a/apps/iir_cascade/CMakeLists.txt b/apps/iir_cascade/CMakeLists.txt new file mode 100644 index 000000000000..54084635323b --- /dev/null +++ b/apps/iir_cascade/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.28) +project(iir_cascade) + +enable_testing() + +# Set up language settings +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED YES) +set(CMAKE_CXX_EXTENSIONS NO) + +# Find Halide +find_package(Halide REQUIRED) + +# Generator +add_halide_generator( + iir_cascade.generator + SOURCES iir_cascade_generator.cpp + LINK_LIBRARIES Halide::Tools +) + +# Filters +add_halide_library( + iir_cascade + FROM iir_cascade.generator + ${IIR_CASCADE_FEATURES} + PARAMS inductive=true +) +add_halide_library( + iir_cascade_noninductive + FROM iir_cascade.generator + GENERATOR iir_cascade + ${IIR_CASCADE_FEATURES} + PARAMS inductive=false +) + +# Main executable +add_executable(iir_cascade_test test.cpp) +target_link_libraries(iir_cascade_test PRIVATE Halide::Tools iir_cascade iir_cascade_noninductive) + +# Test that the app actually works! +add_test(NAME iir_cascade_test COMMAND iir_cascade_test) +set_tests_properties( + iir_cascade_test + PROPERTIES + LABELS iir_cascade + PASS_REGULAR_EXPRESSION "Success!" + SKIP_REGULAR_EXPRESSION "\\[SKIP\\]" +) diff --git a/apps/iir_cascade/Makefile b/apps/iir_cascade/Makefile new file mode 100644 index 000000000000..fe9a8939f087 --- /dev/null +++ b/apps/iir_cascade/Makefile @@ -0,0 +1,31 @@ +include ../support/Makefile.inc + +.PHONY: build clean test + +build: $(BIN)/$(HL_TARGET)/test + +$(GENERATOR_BIN)/iir_cascade.generator: iir_cascade_generator.cpp $(GENERATOR_DEPS) + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) $(filter %.cpp,$^) -o $@ $(LIBHALIDE_LDFLAGS) + +$(BIN)/%/iir_cascade.a: $(GENERATOR_BIN)/iir_cascade.generator + @mkdir -p $(@D) + $^ -g iir_cascade -f iir_cascade -e $(GENERATOR_OUTPUTS) -o $(@D) target=$*-no_runtime inductive=true + +$(BIN)/%/iir_cascade_noninductive.a: $(GENERATOR_BIN)/iir_cascade.generator + @mkdir -p $(@D) + $^ -g iir_cascade -f iir_cascade_noninductive -e $(GENERATOR_OUTPUTS) -o $(@D) target=$*-no_runtime inductive=false + +$(BIN)/%/runtime.a: $(GENERATOR_BIN)/iir_cascade.generator + @mkdir -p $(@D) + $^ -r runtime -o $(@D) target=$* + +$(BIN)/%/test: test.cpp $(BIN)/%/iir_cascade.a $(BIN)/%/iir_cascade_noninductive.a $(BIN)/%/runtime.a + @mkdir -p $(@D) + $(CXX-$*) $(CXXFLAGS-$*) -Wall -O2 -I$(BIN)/$* $^ -o $@ $(LDFLAGS-$*) + +clean: + rm -rf $(BIN) + +test: $(BIN)/$(HL_TARGET)/test + $< diff --git a/apps/iir_cascade/iir_cascade_generator.cpp b/apps/iir_cascade/iir_cascade_generator.cpp new file mode 100644 index 000000000000..d615f257c8a0 --- /dev/null +++ b/apps/iir_cascade/iir_cascade_generator.cpp @@ -0,0 +1,95 @@ +// Creates a cascade of 1D forward-only IIR filters with tanh nonlinearity. +// Contains a version that uses inductive functions, and a version that does not. + +#include "Halide.h" + +using namespace Halide; +using namespace Halide::BoundaryConditions; + +class IIRCascade : public Generator { +public: + Input> input{"input"}; + GeneratorParam N{"N", 4}; // number of filter iterations + GeneratorParam weight{"weight", 0.3f}; // IIR coefficient + GeneratorParam gain{"gain", 3.f}; // gain applied to filter output before nonlinearity + GeneratorParam inductive{"inductive", true}; + Output> output{"output"}; + + void generate() { + Var t("t"), s("s"); + Func in_f("in"); + in_f(t, s) = BoundaryConditions::repeat_edge(input)(t, s); + std::vector filt(N); + RDom rt(1, input.width() - 1, "rt"); + + for (int k = 0; k < N; k++) { + filt[k] = Func(Float(32), "filt" + std::to_string(k)); + Func src; + if (k == 0) { + src = in_f; + } else { + src = filt[k - 1]; + } + if (inductive) { + filt[k](t, s) = select(t <= 0, + weight * src(t, s), + likely((1.f - weight) * filt[k](t - 1, s) + weight * tanh(gain * src(t, s)))); + } else { + filt[k](t, s) = undef(); + filt[k](0, s) = weight * src(0, s); + filt[k](rt, s) = (1.f - weight) * filt[k](rt - 1, s) + weight * tanh(gain * src(rt, s)); + } + } + + output(t, s) = undef(); + RDom ro(0, input.width(), "ro"); + output(ro, s) = tanh(gain * filt[N - 1](ro, s)); + + int VEC; + if (inductive) { + VEC = 32; + } else { + VEC = 4; + } + + Var so("so"), si("si"); + if (get_target().has_feature(Target::CUDA)) { + // Similarly to iir_blur, we can't get parallelism from the recursive dimension. + // The inductive version uses less i/o because it does not have to write the + // entire intermediate filter outputs to global memory. + const int WARP = 32; + output.update().split(s, so, si, WARP).gpu_blocks(so).gpu_lanes(si).reorder(ro, si, so); + for (int k = 0; k < N; k++) { + if (inductive) { + filt[k].fold_storage(t, 2); + filt[k].store_at(output, si).compute_at(output, ro); + } else { + filt[k].compute_at(output, si).reorder_storage(s, t).store_in(MemoryType::Heap); + filt[k].update(1).unroll(rt, 8); + } + } + } else { + // The inductive version is generally not faster on CPU unless the non-linearity is changed to a + // less expensive function and the input is large enough to saturate the last-level cache. + output.split(s, so, si, VEC).vectorize(si); + output.update() + .split(s, so, si, VEC) + .reorder(si, ro, so) + .vectorize(si); + + for (int k = 0; k < N; k++) { + if (inductive) { + filt[k].reorder_storage(s, t).fold_storage(t, 2); + filt[k].store_at(output, so).compute_at(output, ro).vectorize(s, VEC); + } else { + filt[k].compute_at(output, so).reorder_storage(s, t).vectorize(s, VEC).update().vectorize(s, VEC); + filt[k].update(1).vectorize(s, VEC); + } + } + + output.dim(0).set_bounds(0, input.width()); + } + } +}; + +HALIDE_REGISTER_GENERATOR(IIRCascade, iir_cascade) diff --git a/apps/iir_cascade/test.cpp b/apps/iir_cascade/test.cpp new file mode 100644 index 000000000000..75dd92a63844 --- /dev/null +++ b/apps/iir_cascade/test.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include + +#include "HalideBuffer.h" +#include "HalideRuntime.h" + +#include "iir_cascade.h" +#include "iir_cascade_noninductive.h" + +#include "halide_benchmark.h" + +using namespace Halide::Tools; + +int main(int argc, char **argv) { + + const int T = 1024; // number of time steps + const int S = 1024; // number of strips + + Halide::Runtime::Buffer input(T, S); + Halide::Runtime::Buffer out_inductive(T, S); + Halide::Runtime::Buffer out_noninductive(T, S); + + // Set an element to something non-zero, to make sure the generators actually write to the output buffers. + out_inductive(5, 5) = 1.f; + out_noninductive(5, 5) = 2.f; + + input.for_each_element([&](int x, int y) { + input(x, y) = 0.5f * x + 10.0f * sinf(0.01f * x + 0.02f * y); // some arbitrary input signal + }); + + double t_inductive = benchmark([&]() { + iir_cascade(input, out_inductive); + out_inductive.device_sync(); + }); + printf("inductive time: %gms\n", t_inductive * 1e3); + + double t_noninductive = benchmark([&]() { + iir_cascade_noninductive(input, out_noninductive); + out_noninductive.device_sync(); + }); + printf("non-inductive time: %gms\n", t_noninductive * 1e3); + + // out_inductive.copy_to_host(); + // out_noninductive.copy_to_host(); + + float max_err = 0.f; + for (int y = 0; y < S; y++) { + for (int x = 0; x < T; x++) { + max_err = std::max(max_err, std::abs(out_inductive(x, y) - out_noninductive(x, y))); + } + } + printf("max abs difference: %g\n", max_err); + if (max_err > 1e-4f) { + printf("Inductive and non-inductive outputs differ!\n"); + return 1; + } + + printf("Success!\n"); + return 0; +} diff --git a/apps/images/README.md b/apps/images/README.md new file mode 100644 index 000000000000..c3e600673f50 --- /dev/null +++ b/apps/images/README.md @@ -0,0 +1,5 @@ +# Attribution + +aloeL.png and aloeR.png are derived from: D. Scharstein and C. Pal. Learning +conditional random fields for stereo. In IEEE Computer Society Conference on +Computer Vision and Pattern Recognition (CVPR 2007), Minneapolis, MN, June 2007. diff --git a/apps/images/aloeL.png b/apps/images/aloeL.png new file mode 100644 index 000000000000..e8e037c704bf Binary files /dev/null and b/apps/images/aloeL.png differ diff --git a/apps/images/aloeR.png b/apps/images/aloeR.png new file mode 100644 index 000000000000..4edd2ef5cd7c Binary files /dev/null and b/apps/images/aloeR.png differ diff --git a/apps/stereobm/CMakeLists.txt b/apps/stereobm/CMakeLists.txt new file mode 100644 index 000000000000..d23563e300a4 --- /dev/null +++ b/apps/stereobm/CMakeLists.txt @@ -0,0 +1,99 @@ +cmake_minimum_required(VERSION 3.28) +project(stereobm) + +enable_testing() + +# Set up language settings +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED YES) +set(CMAKE_CXX_EXTENSIONS NO) + +option(STEREOBM_BUILD_OPENCV "Build the OpenCV cv::StereoBM comparison for stereobm" ON) +message(STATUS "STEREOBM_BUILD_OPENCV is ${STEREOBM_BUILD_OPENCV}") + +# Find Halide and optionally OpenCV +find_package(Halide REQUIRED) +if (STEREOBM_BUILD_OPENCV) + find_package(OpenCV CONFIG REQUIRED COMPONENTS core imgproc imgcodecs calib3d) +endif () + +# Generator +add_halide_generator(stereobm.generator SOURCES stereobm_generator.cpp LINK_LIBRARIES Halide::Tools) + +# We set parameters here to feed into both OpenCV and our generator, so that they are consistent. +set(STEREOBM_WINSIZE 9) +set(STEREOBM_DEPTH 16) +set(STEREOBM_FILTERCAP 31) +set(STEREOBM_THRESHOLD 10) +set(STEREOBM_UNIQUENESS 0) +set(STEREOBM_MINDISP 0) +set(STEREOBM_TILESIZE 64) + +set(STEREOBM_PARAM_DEFS + WINSIZE=${STEREOBM_WINSIZE} + DEPTH=${STEREOBM_DEPTH} + PREFILTER_CAP=${STEREOBM_FILTERCAP} + TEXTURE_THRESHOLD=${STEREOBM_THRESHOLD} + UNIQUENESS_RATIO=${STEREOBM_UNIQUENESS} + MIN_DISP=${STEREOBM_MINDISP} +) + +add_halide_library( + stereobm + FROM stereobm.generator + PARAMS + winsize=${STEREOBM_WINSIZE} + depth=${STEREOBM_DEPTH} + tilesize=${STEREOBM_TILESIZE} + threshold=${STEREOBM_THRESHOLD} + mindisp=${STEREOBM_MINDISP} + uniqueness_ratio=${STEREOBM_UNIQUENESS} + filtercap=${STEREOBM_FILTERCAP} +) + +# Always build the variant without OpenCV. +add_executable(stereobm_process process.cpp) +target_link_libraries(stereobm_process PRIVATE Halide::ImageIO stereobm) +target_compile_definitions(stereobm_process PRIVATE STEREOBM_BUILD_OPENCV=0 ${STEREOBM_PARAM_DEFS}) + +# Build the OpenCV variant if available. +if (STEREOBM_BUILD_OPENCV) + add_executable(stereobm_process_opencv process.cpp) + target_link_libraries(stereobm_process_opencv PRIVATE Halide::ImageIO stereobm ${OpenCV_LIBS}) + target_compile_definitions( + stereobm_process_opencv + PRIVATE STEREOBM_BUILD_OPENCV=1 ${STEREOBM_PARAM_DEFS} + ) +endif () + +# Test that the app actually works! +set(IMAGEL ${CMAKE_CURRENT_LIST_DIR}/../images/aloeL.png) +set(IMAGER ${CMAKE_CURRENT_LIST_DIR}/../images/aloeR.png) + +if (EXISTS ${IMAGEL} AND EXISTS ${IMAGER}) + configure_file(${IMAGEL} aloeL.png COPYONLY) + configure_file(${IMAGER} aloeR.png COPYONLY) + + add_test(NAME stereobm_process COMMAND stereobm_process aloeL.png aloeR.png out_nocv.png) + set_tests_properties( + stereobm_process + PROPERTIES + LABELS stereobm + PASS_REGULAR_EXPRESSION "Success!" + SKIP_REGULAR_EXPRESSION "\\[SKIP\\]" + ) + + if (STEREOBM_BUILD_OPENCV) + add_test( + NAME stereobm_process_opencv + COMMAND stereobm_process_opencv aloeL.png aloeR.png out.png + ) + set_tests_properties( + stereobm_process_opencv + PROPERTIES + LABELS stereobm + PASS_REGULAR_EXPRESSION "Success!" + SKIP_REGULAR_EXPRESSION "\\[SKIP\\]" + ) + endif () +endif () diff --git a/apps/stereobm/Makefile b/apps/stereobm/Makefile new file mode 100644 index 000000000000..f62039579923 --- /dev/null +++ b/apps/stereobm/Makefile @@ -0,0 +1,64 @@ +include ../support/Makefile.inc + +# process.cpp compares against OpenCV's StereoBM. +# Auto-disable if opencv4 is not available via pkg-config. +HAVE_OPENCV4 := $(shell pkg-config --exists opencv4 2>/dev/null && echo yes) +STEREOBM_BUILD_OPENCV ?= $(if $(HAVE_OPENCV4),1,0) +ifeq ($(STEREOBM_BUILD_OPENCV),1) + OPENCV_CXXFLAGS := $(shell pkg-config --cflags opencv4 2>/dev/null) + OPENCV_LIBS := $(shell pkg-config --libs opencv4 2>/dev/null) +endif + +# We set parameters here to feed into both OpenCV and our generator, so that they are consistent. +STEREOBM_WINSIZE ?= 9 +STEREOBM_DEPTH ?= 16 +STEREOBM_FILTERCAP ?= 31 +STEREOBM_THRESHOLD ?= 10 +STEREOBM_UNIQUENESS ?= 0 +STEREOBM_MINDISP ?= 0 +STEREOBM_TILESIZE ?= 64 + +STEREOBM_GEN_PARAMS := winsize=$(STEREOBM_WINSIZE) depth=$(STEREOBM_DEPTH) tilesize=$(STEREOBM_TILESIZE) threshold=$(STEREOBM_THRESHOLD) mindisp=$(STEREOBM_MINDISP) uniqueness_ratio=$(STEREOBM_UNIQUENESS) filtercap=$(STEREOBM_FILTERCAP) + +STEREOBM_DEFINES := -DWINSIZE=$(STEREOBM_WINSIZE) -DDEPTH=$(STEREOBM_DEPTH) -DPREFILTER_CAP=$(STEREOBM_FILTERCAP) -DTEXTURE_THRESHOLD=$(STEREOBM_THRESHOLD) -DUNIQUENESS_RATIO=$(STEREOBM_UNIQUENESS) -DMIN_DISP=$(STEREOBM_MINDISP) + +# Always build and test the no-OpenCV variant; add OpenCV if available +BUILD_TARGETS := $(BIN)/$(HL_TARGET)/process +TEST_OUTPUTS := $(BIN)/$(HL_TARGET)/out.png +ifeq ($(STEREOBM_BUILD_OPENCV),1) + BUILD_TARGETS += $(BIN)/$(HL_TARGET)/process_opencv + TEST_OUTPUTS += $(BIN)/$(HL_TARGET)/out2.png +endif + +.PHONY: build clean test + +build: $(BUILD_TARGETS) + +$(GENERATOR_BIN)/stereobm.generator: stereobm_generator.cpp $(GENERATOR_DEPS) + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) $(filter %.cpp,$^) -o $@ $(LIBHALIDE_LDFLAGS) + +$(BIN)/%/stereobm.a: $(GENERATOR_BIN)/stereobm.generator + @mkdir -p $(@D) + $^ -g stereobm -e $(GENERATOR_OUTPUTS) -o $(@D) -f stereobm target=$* $(STEREOBM_GEN_PARAMS) + +$(BIN)/%/process_opencv: process.cpp $(BIN)/%/stereobm.a + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) -Wno-cast-qual -DSTEREOBM_BUILD_OPENCV=1 $(STEREOBM_DEFINES) $(OPENCV_CXXFLAGS) -I$(BIN)/$* -Wall -O3 $^ -o $@ $(LDFLAGS) $(IMAGE_IO_FLAGS) $(CUDA_LDFLAGS) $(OPENCL_LDFLAGS) $(OPENCV_LIBS) + +$(BIN)/%/process: process.cpp $(BIN)/%/stereobm.a + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) -DSTEREOBM_BUILD_OPENCV=0 $(STEREOBM_DEFINES) -I$(BIN)/$* -Wall -O3 $^ -o $@ $(LDFLAGS) $(IMAGE_IO_FLAGS) $(CUDA_LDFLAGS) $(OPENCL_LDFLAGS) + +$(BIN)/%/out.png: $(BIN)/%/process_opencv + @mkdir -p $(@D) + $< $(IMAGES)/aloeL.png $(IMAGES)/aloeR.png $@ + +$(BIN)/%/out2.png: $(BIN)/%/process + @mkdir -p $(@D) + $< $(IMAGES)/aloeL.png $(IMAGES)/aloeR.png $@ + +clean: + rm -rf $(BIN) + +test: $(TEST_OUTPUTS) diff --git a/apps/stereobm/process.cpp b/apps/stereobm/process.cpp new file mode 100644 index 000000000000..e43d94e16dc4 --- /dev/null +++ b/apps/stereobm/process.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include +#include + +#include "HalideBuffer.h" +#include "HalideRuntime.h" + +#include "stereobm.h" + +#include "halide_benchmark.h" +#include "halide_image_io.h" + +#if STEREOBM_BUILD_OPENCV +#include +#include +#include +#endif + +using namespace Halide::Tools; + +#if STEREOBM_BUILD_OPENCV +#ifndef WINSIZE +static constexpr int WINSIZE = 9; +#endif +#ifndef DEPTH +static constexpr int DEPTH = 16; +#endif +#ifndef PREFILTER_CAP +static constexpr int PREFILTER_CAP = 31; +#endif +#ifndef TEXTURE_THRESHOLD +static constexpr int TEXTURE_THRESHOLD = 10; +#endif +#ifndef UNIQUENESS_RATIO +static constexpr int UNIQUENESS_RATIO = 0; +#endif +#ifndef MIN_DISP +static constexpr int MIN_DISP = 0; +#endif +#endif + +int main(int argc, char **argv) { + if (argc != 4) { + printf("Usage: %s left right out\n", argv[0]); + return 1; + } + + Halide::Runtime::Buffer left, right; + +#if STEREOBM_BUILD_OPENCV + // Load a pair of 3-channel images and convert to grayscale with OpenCV + cv::Mat left_color = cv::imread(argv[1], cv::IMREAD_COLOR); + cv::Mat right_color = cv::imread(argv[2], cv::IMREAD_COLOR); + if (left_color.empty() || right_color.empty()) { + fprintf(stderr, "OpenCV could not read %s / %s\n", argv[1], argv[2]); + return 1; + } + cv::Mat cv_left, cv_right; + cv::cvtColor(left_color, cv_left, cv::COLOR_BGR2GRAY); + cv::cvtColor(right_color, cv_right, cv::COLOR_BGR2GRAY); + + auto gray_to_buffer = [](const cv::Mat &g) { + Halide::Runtime::Buffer buf(g.cols, g.rows); + buf.for_each_element([&](int i, int j) { + buf(i, j) = g.at(j, i); + }); + return buf; + }; + left = gray_to_buffer(cv_left); + right = gray_to_buffer(cv_right); +#else + // Without OpenCV, load with Halide's image IO and manually convert to grayscale + auto load_gray = [](const char *path) { + Halide::Runtime::Buffer color = load_and_convert_image(path); + Halide::Runtime::Buffer gray(color.width(), color.height()); + gray.for_each_element([&](int i, int j) { + uint32_t r = color(i, j, 0), g = color(i, j, 1), b = color(i, j, 2); + gray(i, j) = (uint8_t)((r * 4899 + g * 9617 + b * 1868 + 8192) >> 14); + }); + return gray; + }; + left = load_gray(argv[1]); + right = load_gray(argv[2]); +#endif + + Halide::Runtime::Buffer output(left.width(), left.height()); + + double best = benchmark([&]() { + stereobm(left, right, output); + output.device_sync(); + }); + printf("Halide time: %gms\n", best * 1e3); + +#if STEREOBM_BUILD_OPENCV + cv::Ptr bm = cv::StereoBM::create(DEPTH, WINSIZE); + bm->setPreFilterType(cv::StereoBM::PREFILTER_XSOBEL); + bm->setPreFilterCap(PREFILTER_CAP); + bm->setMinDisparity(MIN_DISP); + bm->setTextureThreshold(TEXTURE_THRESHOLD); + bm->setUniquenessRatio(UNIQUENESS_RATIO); + + cv::Mat cv_disp; + double cv_best = benchmark([&]() { + bm->compute(cv_left, cv_right, cv_disp); + }); + printf("OpenCV time: %gms\n", cv_best * 1e3); + + const int32_t invalid = (MIN_DISP - 1) * 16; + long long compared = 0; + long long sum_abs = 0; + int max_abs = 0; + if (output.width() != cv_disp.cols || output.height() != cv_disp.rows) { + fprintf(stderr, "Output size %d x %d doesn't match OpenCV's output size %d x %d\n", + output.width(), output.height(), cv_disp.cols, cv_disp.rows); + return 1; + } + output.for_each_element([&](int i, int j) { + int32_t h = output(i, j); + int32_t o = cv_disp.at(j, i); + if (h == invalid || o == invalid) return; + int diff = std::abs(h - o); + compared++; + sum_abs += diff; + max_abs = std::max(max_abs, diff); + }); + + if (compared > 0) { + printf("compared %lld pixels\n", compared); + printf("mean abs diff: %g px\n", + (sum_abs / (double)compared) / 16.0); + printf("max abs diff: %g px\n", max_abs / 16.0); + } else { + printf("no overlapping valid pixels to compare\n"); + } +#endif + + // normalize output to range [0, 255] + int32_t dmin = INT32_MAX, dmax = INT32_MIN; + output.for_each_value([&](int16_t v) { + dmin = std::min(dmin, (int32_t)v); + dmax = std::max(dmax, (int32_t)v); + }); + double scale = (dmax > dmin) ? 255.0 / (dmax - dmin) : 0.0; + Halide::Runtime::Buffer vis(output.width(), output.height()); + vis.for_each_element([&](int i, int j) { + int n = (output(i, j) - dmin) * scale + 0.5; + vis(i, j) = std::max(0, std::min(255, n)); + }); + convert_and_save_image(vis, argv[3]); + + printf("Success!\n"); + return 0; +} diff --git a/apps/stereobm/stereobm_generator.cpp b/apps/stereobm/stereobm_generator.cpp new file mode 100644 index 000000000000..cc4ca3cf69e7 --- /dev/null +++ b/apps/stereobm/stereobm_generator.cpp @@ -0,0 +1,204 @@ +// M*////////////////////////////////////////////////////////////////////////////////////// +// +// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. +// +// By downloading, copying, installing or using the software you agree to this license. +// If you do not agree to this license, do not download, install, +// copy or use the software. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright (C) 2000, Intel Corporation, all rights reserved. +// Copyright (C) 2013, OpenCV Foundation, all rights reserved. +// Third party copyrights are property of their respective owners. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's 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. +// +// * The name of the copyright holders may not 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 Intel Corporation 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. +// +// M*/ + +// Halide adaptation of https://github.com/opencv/opencv/blob/4.x/modules/calib3d/src/stereobm.cpp + +#include "Halide.h" + +using namespace Halide; +using namespace Halide::BoundaryConditions; + +class StereoBM : public Generator { +public: + // Inputs: two grayscale images of the same scene as seen by a left and right camera + Input> left_gray{"left_gray"}; + Input> right_gray{"right_gray"}; + + GeneratorParam winsize{"winsize", 9}; // size of block surrounding each pixel to compare; must be odd + GeneratorParam depth{"depth", 16}; // maximum number of disparities to consider + GeneratorParam tilesize{"tilesize", 64}; // strip size + GeneratorParam threshold{"threshold", 10}; // reject pixels if sum of prefiltered pixels in block is less than this + GeneratorParam mindisp{"mindisp", 0}; // minimum disparity to consider. + GeneratorParam uniqueness_ratio{"uniqueness_ratio", 0}; // reject if the second-best match is too close to the best match, as a percentage of the best match score. + GeneratorParam filtercap{"filtercap", 31}; // clamp the prefiltering output to the range [0, filtercap*2]. + Output> output{"output"}; + + void generate() { + const Type uint16 = UInt(16); + const Type int16 = Int(16); + const Type int32 = Int(32); + const int native_lanes = get_target().natural_vector_size(); + + Var y("y"); + Var x("x"); + Var c("c"); + Var di("di"); + Var xi("xi"), xo("xo"); + + Expr W = left_gray.dim(0).extent(); + Expr H = left_gray.dim(1).extent(); + + Func proc0("proc0"), proc1("proc1"); + // shift the input to ensure the indices of blur_y correspond to the center pixel in the window. + proc0(x, y) = cast(BoundaryConditions::mirror_interior(left_gray)(x - winsize / 2, y - winsize / 2)); + proc1(x, y) = cast(BoundaryConditions::mirror_interior(right_gray)(x - winsize / 2, y - winsize / 2)); + + Func b0("b0"), b1("b1"); + + // prefiltering with sobel filter + Func xsobel0("xsobel0"), xsobel1("xsobel1"); + Expr e0 = proc0(x + 1, y - 1) - proc0(x - 1, y - 1) + 2 * proc0(x + 1, y) - 2 * proc0(x - 1, y) + proc0(x + 1, y + 1) - proc0(x - 1, y + 1); + Expr e1 = proc1(x + 1, y - 1) - proc1(x - 1, y - 1) + 2 * proc1(x + 1, y) - 2 * proc1(x - 1, y) + proc1(x + 1, y + 1) - proc1(x - 1, y + 1); + Expr ix = x - winsize / 2, iy = y - winsize / 2; // image-space coordinates + Expr border = ix == 0 || ix == W - 1 || ((H % 2 == 1) && iy == H - 1); + xsobel0(x, y) = select(border, cast(filtercap), cast(clamp(e0, -1 * filtercap, filtercap) + filtercap)); + xsobel1(x, y) = select(border, cast(filtercap), cast(clamp(e1, -1 * filtercap, filtercap) + filtercap)); + + Func diff("diff"); + // for each disparity di, sample right image (depth-1-di)+mindisp pixels left of the left image's pixel + // image is divided into strips indexed by xo that are processed in parallel + diff(di, xi, y, xo) = Halide::cast(cast(abs((xsobel0(xi + xo * tilesize, y)) - (xsobel1(xi + xo * tilesize + di - depth + 1 - mindisp, y))))); + + // compute the sum of absolute differences (SAD) for each pixel and disparity + Func vsum(uint16, std::string("vsum")); + Func zero_blur(uint16, "zero_blur"); + RDom rwin(0, winsize, "rwin"); + zero_blur(di, xi, xo) = sum(cast(diff(di, xi, rwin, xo))); + vsum(di, xi, y, xo) = select(y <= 0, zero_blur(di, xi, xo), likely(vsum(di, xi, y - 1, xo) + diff(di, xi, y + winsize - 1, xo) - diff(di, xi, y - 1, xo))); + Func blur_y(uint16, "blur_y"); + RDom rx(0, tilesize, "rx"); + Func f1("f1"); + f1(di, y, xo) = sum(vsum(di, rwin, y, xo)); + blur_y(di, xi, y, xo) = select(xi <= 0, f1(di, y, xo), likely(blur_y(di, xi - 1, y, xo) + vsum(di, xi + winsize - 1, y, xo) - vsum(di, xi - 1, y, xo))); + + // compute the texture value for each pixel, which is the sum of pixels in the surrounding block + Func text("text"), zerotext("zerotext"), textf1("textf1"); + Func textsum(uint16, "textsum"); // explicit type required for inductive self-reference + Func textblury(uint16, "textblury"); // explicit type required for inductive self-reference + text(xi, y, xo) = cast(abs(cast(xsobel0(xi + xo * tilesize, y)) - cast(filtercap))); + zerotext(xi, xo) = sum(cast(text(xi, rwin, xo))); + textsum(xi, y, xo) = select(y <= 0, zerotext(xi, xo), likely(textsum(xi, y - 1, xo) + text(xi, y + winsize - 1, xo) - text(xi, y - 1, xo))); + textf1(y, xo) = sum(textsum(rwin, y, xo)); + textblury(xi, y, xo) = select(xi <= 0, textf1(y, xo), likely(textblury(xi - 1, y, xo) + textsum(xi + winsize - 1, y, xo) - textsum(xi - 1, y, xo))); + + // compute the best and second-best disparity for each pixel + Func preout("preout"); + RDom rd(0, depth, "rd"); + preout(xi, y, xo) = cast(65535); + preout(xi, y, xo) = min(blur_y(rd, xi, y, xo), preout(xi, y, xo)); + Func prearg("prearg"); + prearg(di, xi, y, xo) = select(preout(xi, y, xo) == blur_y(di, xi, y, xo), cast(di), cast(65535)); + Func argmin1("argmin1"); + argmin1(xi, y, xo) = cast(65535); + argmin1(xi, y, xo) = min(argmin1(xi, y, xo), prearg(rd, xi, y, xo)); + Func second_best("second_best"); + second_best(di, xi, y, xo) = select(abs(di - cast(argmin1(xi, y, xo))) <= 1, cast(65535), blur_y(di, xi, y, xo)); + Func argmin2("argmin2"); + argmin2(xi, y, xo) = cast(65535); + argmin2(xi, y, xo) = min(argmin2(xi, y, xo), second_best(rd, xi, y, xo)); + Func p_clamped("p_clamped"); + p_clamped(xi, y, xo) = clamp(argmin1(xi, y, xo), 1, depth - 2); // indexes into blur_y + + // subpixel refinement + Func subpout(int16, "subpout"); + Expr p = cast(blur_y(cast(int32, p_clamped(xi, y, xo)) + 1, xi, y, xo)); + Expr n = cast(blur_y(cast(int32, p_clamped(xi, y, xo)) - 1, xi, y, xo)); + Expr d1 = p + n - 2 * preout(xi, y, xo) + abs(p - n); + Expr q = (abs(p - n) * 256) / d1; + Expr quot = select(p >= n, q, -q); + Expr subpout_expr = cast((cast(depth - p_clamped(xi, y, xo) - 1 + mindisp) * 256 + (select(d1 == 0, 0, quot) + 15)) >> 4); + subpout(xi, y, xo) = select(argmin1(xi, y, xo) > 0 && argmin1(xi, y, xo) < depth - 1, subpout_expr, cast((depth - argmin1(xi, y, xo) - 1 + mindisp) * 16)); + + // edge case handling + Expr filtered = cast((mindisp - 1) * 16); + Expr reject = textblury(xi, y, xo) < threshold; // reject if block texture is too uniform + if (int(uniqueness_ratio) > 0) { // reject if second-best disparity is too close to best disparity + reject = reject || (cast(argmin2(xi, y, xo)) <= cast(preout(xi, y, xo)) + (cast(preout(xi, y, xo)) * cast(uniqueness_ratio)) / 100); + } + Func splitoutput("splitoutput"); + splitoutput(xi, y, xo) = select(reject, filtered, subpout(xi, y, xo)); + + // disparities are only valid where the SAD window and the full disparity search both fit inside the image. + Expr sw2 = winsize / 2; + Expr in_valid_roi = x >= (static_cast(mindisp) + static_cast(depth) - 1) + sw2 && x < W - static_cast(mindisp) - sw2 && + y >= sw2 && y < H - sw2; + output(x, y) = select(in_valid_roi, splitoutput(x % tilesize, y, x / tilesize), filtered); + + proc0.compute_root().vectorize(x, native_lanes * 4); + proc1.compute_root().vectorize(x, native_lanes * 4); + xsobel0.compute_root().vectorize(x, native_lanes * 4); + xsobel1.compute_root().vectorize(x, native_lanes * 4); + + preout.compute_at(splitoutput, xi).update().atomic(false).vectorize(rd, depth); + argmin1.compute_at(splitoutput, xi).update().atomic(false).vectorize(rd, depth); + argmin2.compute_at(splitoutput, xi).update().atomic(false).vectorize(rd, depth); + + subpout.compute_at(splitoutput, xi).vectorize(xi, native_lanes); + splitoutput.compute_root().reorder_storage(xi, xo, y).parallel(xo).vectorize(xi, native_lanes); + + blur_y.bound(di, 0, depth); + vsum.bound(di, 0, depth); + + blur_y.compute_at(splitoutput, y).store_at(splitoutput, y).vectorize(di, depth).fold_storage(xi, 1); + vsum.compute_at(splitoutput, y).store_at(splitoutput, xo).vectorize(di, depth).fold_storage(y, 1); + + f1.compute_at(splitoutput, y).vectorize(di, depth); + zero_blur.compute_at(splitoutput, xo).vectorize(di, depth); + + zerotext.compute_at(splitoutput, xo).vectorize(xi, native_lanes); + textsum.compute_at(splitoutput, y).store_at(splitoutput, xo).vectorize(xi).fold_storage(y, 1); + textf1.compute_at(splitoutput, y); + textblury.compute_at(splitoutput, y).store_at(splitoutput, y).fold_storage(xi, 1); + textblury.compute_with(blur_y, xi); + + // Constrain the output origin to 0 so the base-case boundary terms + // (min(output.min.1, 0), select(output.min.1 <= y, ...)) fold to + // constants, letting the inductive fold analysis simplify the y + // footprint of textsum/vsum to a clean single-element step. + output.dim(0).set_min(0); + output.dim(1).set_min(0); + + output.vectorize(x, native_lanes); + } +}; + +HALIDE_REGISTER_GENERATOR(StereoBM, stereobm) diff --git a/apps/vcpkg.json b/apps/vcpkg.json index 1c3c5805f7ca..1c22114d1063 100644 --- a/apps/vcpkg.json +++ b/apps/vcpkg.json @@ -21,6 +21,7 @@ "name": "opencl", "platform": "(windows & x64 & !uwp & !xbox) | (linux & x64) | (linux & arm64)" }, + "opencv4", "protobuf", "pybind11", "tensorflow-lite" diff --git a/python_bindings/test/correctness/basics.py b/python_bindings/test/correctness/basics.py index 15bc76087e77..886a8e786eba 100644 --- a/python_bindings/test/correctness/basics.py +++ b/python_bindings/test/correctness/basics.py @@ -500,15 +500,6 @@ def test_unevaluated_funcref(): f[x] += 1 assert f.realize([1])[0] == 1 - with assert_throws( - hl.HalideError, - r"Error: Can't call Func \"f(\$\d+)?\" because it has not yet been defined\.", - ): - # This is invalid because we only allow unevaluated func refs on the LHS of a - # binary operator. - f = hl.Func("f") - f[x] = 1 + f[x] - with assert_throws( hl.HalideError, r"Cannot use an unevaluated reference to 'f(\$\d+)?' to define an update at a different location\.", diff --git a/src/Bounds.cpp b/src/Bounds.cpp index 9087516b54b8..ba9ba69975a9 100644 --- a/src/Bounds.cpp +++ b/src/Bounds.cpp @@ -3286,7 +3286,7 @@ FuncValueBounds compute_function_value_bounds(const vector &order, Interval result; - if (f.is_pure()) { + if (f.is_pure() && !f.is_inductive()) { // Make a scope that says the args could be anything. Scope arg_scope; diff --git a/src/BoundsInference.cpp b/src/BoundsInference.cpp index 8befb2c0454e..07e0af20b04a 100644 --- a/src/BoundsInference.cpp +++ b/src/BoundsInference.cpp @@ -7,6 +7,7 @@ #include "IREquality.h" #include "IRMutator.h" #include "IROperator.h" +#include "Inductive.h" #include "Inline.h" #include "Qualify.h" #include "Scope.h" @@ -386,6 +387,7 @@ class BoundsInference : public IRMutator { // don't care what sites are loaded, just what sites need // to have the correct value in them. So remap all selects // to if_then_elses to get tighter bounds. + // The implementation of expand_to_base_case relies on this conversion. class SelectToIfThenElse : public IRMutator { using IRMutator::visit; Expr visit(const Select *op) override { @@ -1002,6 +1004,21 @@ class BoundsInference : public IRMutator { } } + // For any inductively defined functions, make sure their + // bounds include the base case. + for (Stage &s : stages) { + if (!s.func.is_pure() || !s.func.is_inductive()) { + continue; + } + debug(4) << "Expanding bounds for inductively defined function " << s.func.name() << "\n"; + for (const auto &b1 : s.bounds) { + // const Box &b = b1.second; + for (const auto &cval : s.exprs) { + s.bounds[b1.first] = expand_to_include_base_case(s.func.args(), cval.value, s.func.name(), s.bounds[b1.first]); + } + } + } + // The region required of the each output is expanded to include the size of the output buffer. for (const Function &output : outputs) { Box output_box; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e2229eca2c7..ec9897dd434c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -132,6 +132,7 @@ target_sources( HexagonOffload.h HexagonOptimize.h ImageParam.h + Inductive.h InferArguments.h InjectHostDevBufferCopies.h Inline.h @@ -313,6 +314,7 @@ target_sources( HexagonOffload.cpp HexagonOptimize.cpp ImageParam.cpp + Inductive.cpp InferArguments.cpp InjectHostDevBufferCopies.cpp Inline.cpp diff --git a/src/Deserialization.cpp b/src/Deserialization.cpp index 8a27cc8c1386..d4b650121834 100644 --- a/src/Deserialization.cpp +++ b/src/Deserialization.cpp @@ -382,6 +382,8 @@ DimType Deserializer::deserialize_dim_type(Serialize::DimType dim_type) { return DimType::PureRVar; case Serialize::DimType::ImpureRVar: return DimType::ImpureRVar; + case Serialize::DimType::InductiveVar: + return DimType::InductiveVar; default: user_error << "unknown dim type " << (int)dim_type << "\n"; return DimType::PureVar; diff --git a/src/Func.cpp b/src/Func.cpp index 81c6c7cfc7fa..a1309640f3cb 100644 --- a/src/Func.cpp +++ b/src/Func.cpp @@ -67,10 +67,18 @@ Func::Func(const string &name) : func(unique_name(name)) { } +Func::Func(const Type &required_type, const string &name) + : func({required_type}, AnyDims, unique_name(name)) { +} + Func::Func(const Type &required_type, int required_dims, const string &name) : func({required_type}, required_dims, unique_name(name)) { } +Func::Func(const std::vector &required_types, const string &name) + : func(required_types, AnyDims, unique_name(name)) { +} + Func::Func(const std::vector &required_types, int required_dims, const string &name) : func(required_types, required_dims, unique_name(name)) { } @@ -492,7 +500,7 @@ void Stage::set_dim_type(const VarOrRVar &var, ForType t) { // If it's an rvar and the for type is parallel, we need to // validate that this doesn't introduce a race condition, // unless it is flagged explicitly or is a associative atomic operation. - if (!dim.is_pure() && var.is_rvar && is_parallel(t)) { + if (!dim.is_pure() && (var.is_rvar || dim.is_inductive()) && is_parallel(t)) { if (!definition.schedule().allow_race_conditions() && definition.schedule().atomic()) { if (!definition.schedule().override_atomic_associativity_test()) { @@ -1374,6 +1382,8 @@ Stage &Stage::fuse(const VarOrRVar &inner, const VarOrRVar &outer, const VarOrRV dims[inner_pos].dim_type = DimType::ImpureRVar; } else if (dims[inner_pos].dim_type == DimType::PureRVar || outer_type == DimType::PureRVar) { dims[inner_pos].dim_type = DimType::PureRVar; + } else if (dims[inner_pos].dim_type == DimType::InductiveVar || outer_type == DimType::InductiveVar) { + dims[inner_pos].dim_type = DimType::InductiveVar; } else { dims[inner_pos].dim_type = DimType::PureVar; } @@ -3400,24 +3410,39 @@ Stage FuncRef::operator/=(const FuncRef &e) { } FuncRef::operator Expr() const { + /* user_assert(func.has_pure_definition() || func.has_extern_definition()) << "Can't call Func \"" << func.name() << "\" because it has not yet been defined.\n"; + */ - user_assert(func.outputs() == 1) + user_assert(func.required_types().empty() || func.outputs() == 1) << "Can't convert a reference Func \"" << func.name() << "\" to an Expr, because " << func.name() << " returns a Tuple.\n"; + if (!(func.has_pure_definition() || func.has_extern_definition())) { + Type t = Type(Type::Unknown, 0, 1); + if (!func.required_types().empty()) { + t = func.required_types()[0]; + } + return Call::make(t, func.name(), args, Call::Halide, + func.get_contents(), 0, Buffer<>(), Parameter()); + } + return Call::make(func, args); } FuncTupleElementRef FuncRef::operator[](int i) const { - user_assert(func.has_pure_definition() || func.has_extern_definition()) - << "Can't call Func \"" << func.name() << "\" because it has not yet been defined.\n"; + /*user_assert(func.has_pure_definition() || func.has_extern_definition()) + << "Can't call Func \"" << func.name() << "\" because it has not yet been defined.\n"; */ user_assert(func.outputs() != 1) << "Can't index into a reference to Func \"" << func.name() << "\", because it does not return a Tuple.\n"; + user_assert(!func.required_types().empty() || !func.output_types().empty()) + << "Can't index into a reference to Func \"" << func.name() + << "\", because it does not have an explicitly defined type.\n"; + user_assert(i >= 0 && i < func.outputs()) << "Tuple index out of range in reference to Func \"" << func.name() << "\".\n"; diff --git a/src/Func.h b/src/Func.h index 0bfb591871c7..687c2fead2c5 100644 --- a/src/Func.h +++ b/src/Func.h @@ -756,12 +756,21 @@ class Func { /** Declare a new undefined function with the given name */ explicit Func(const std::string &name); + /** Declare a new undefined function with the given name. + * The function will be constrained to represent Exprs of required_type. */ + explicit Func(const Type &required_type, const std::string &name); + /** Declare a new undefined function with the given name. * The function will be constrained to represent Exprs of required_type. * If required_dims is not AnyDims, the function will be constrained to exactly * that many dimensions. */ explicit Func(const Type &required_type, int required_dims, const std::string &name); + /** Declare a new undefined function with the given name. + * If required_types is not empty, the function will be constrained to represent + * Tuples of the same arity and types. (If required_types is empty, there is no constraint.) */ + explicit Func(const std::vector &required_types, const std::string &name); + /** Declare a new undefined function with the given name. * If required_types is not empty, the function will be constrained to represent * Tuples of the same arity and types. (If required_types is empty, there is no constraint.) diff --git a/src/Function.cpp b/src/Function.cpp index d9484e5aca0d..dd9528920443 100644 --- a/src/Function.cpp +++ b/src/Function.cpp @@ -205,9 +205,10 @@ struct CheckVars : public IRGraphVisitor { Scope<> defined_internally; const std::string name; bool unbound_reduction_vars_ok = false; + bool pure; - CheckVars(const std::string &n) - : name(n) { + CheckVars(const std::string &n, bool pure) + : name(n), pure(pure) { } using IRVisitor::visit; @@ -220,15 +221,34 @@ struct CheckVars : public IRGraphVisitor { void visit(const Call *op) override { IRGraphVisitor::visit(op); - if (op->name == name && op->call_type == Call::Halide) { - for (size_t i = 0; i < op->args.size(); i++) { - const Variable *var = op->args[i].as(); - if (!pure_args[i].empty()) { - user_assert(var && var->name == pure_args[i]) - << "In definition of Func \"" << name << "\":\n" - << "All of a function's recursive references to itself" - << " must contain the same pure variables in the same" - << " places as on the left-hand-side.\n"; + bool proper_func = (name == op->name || op->call_type != Call::Halide); + if (op->call_type == Call::Halide && + op->func.defined() && + op->name != name) { + Function func = Function(op->func); + proper_func = (name == op->name || func.has_pure_definition() || func.has_extern_definition()); + } + if (pure) { + user_assert(proper_func) + << "In pure definition of Func \"" << name << "\":\n" + << "Can't call Func \"" << op->name + << "\" because it has not yet been defined," + << " and it is not a recursive call.\n"; + } else { + user_assert(proper_func) + << "In update definition of Func \"" << name << "\":\n" + << "Can't call Func \"" << op->name + << "\" because it has not yet been defined.\n"; + if (op->name == name && op->call_type == Call::Halide) { + for (size_t i = 0; i < op->args.size(); i++) { + const Variable *var = op->args[i].as(); + if (!pure_args[i].empty()) { + user_assert(var && var->name == pure_args[i]) + << "In update definition of Func \"" << name << "\":\n" + << "All of a function's recursive references to itself" + << " in update definitions must contain the same pure" + << " variables in the same places as on the left-hand-side.\n"; + } } } } @@ -560,7 +580,7 @@ void Function::define(const vector &args, vector values) { // Make sure all the vars in the value are either args or are // attached to some parameter - CheckVars check(name()); + CheckVars check(name(), true); check.pure_args = args; for (const auto &value : values) { value.accept(&check); @@ -568,6 +588,7 @@ void Function::define(const vector &args, vector values) { // Freeze all called functions FreezeFunctions freezer(name()); + // TODO: Check for calls to undefined Funcs for (const auto &value : values) { value.accept(&freezer); } @@ -627,11 +648,27 @@ void Function::define(const vector &args, vector values) { init_def_args[i] = Var(args[i]); } + // If the function is inductive, + // the value and args might refer back to the + // function itself, introducing circular references and hence + // memory leaks. We need to break these cycles. + WeakenFunctionPtrs weakener(contents.get()); + for (auto &arg : init_def_args) { + arg = weakener(arg); + } + for (auto &value : values) { + value = weakener(value); + } + ReductionDomain rdom; contents->init_def = Definition(init_def_args, values, rdom, true); for (const auto &arg : args) { - Dim d = {arg, ForType::Serial, DeviceAPI::None, DimType::PureVar}; + DimType dtype = DimType::PureVar; + if (is_inductive(arg)) { + dtype = DimType::InductiveVar; + } + Dim d = {arg, ForType::Serial, DeviceAPI::None, dtype}; contents->init_def.schedule().dims().push_back(d); StorageDim sd = {arg}; contents->func_schedule.storage_dims().push_back(sd); @@ -687,6 +724,9 @@ void Function::define_update(const vector &_args, vector values, con user_assert(!frozen()) << "Func " << name() << " cannot be given a new update definition, " << "because it has already been realized or used in the definition of another Func.\n"; + user_assert(!is_inductive()) + << "In update definition " << update_idx << " of Func \"" << name() << "\":\n" + << "Inductive functions cannot have update definitions.\n"; for (auto &value : values) { user_assert(value.defined()) @@ -757,7 +797,7 @@ void Function::define_update(const vector &_args, vector values, con // pure args, in the reduction domain, or a parameter. Also checks // that recursive references to the function contain all the pure // vars in the LHS in the correct places. - CheckVars check(name()); + CheckVars check(name(), false); check.pure_args = pure_args; for (const auto &arg : args) { arg.accept(&check); @@ -993,7 +1033,9 @@ int Function::dimensions() const { } int Function::outputs() const { - return (int)output_types().size(); + return (has_pure_definition() || has_extern_definition()) ? + (int)output_types().size() : + (int)required_types().size(); } const std::vector &Function::output_types() const { @@ -1064,8 +1106,65 @@ bool Function::has_pure_definition() const { return contents->init_def.defined(); } +bool Function::is_inductive() const { + if (!has_pure_definition()) { + return false; + } + + bool recursive = false; + for (const Expr &e : definition().values()) { + visit_with(e, [&](auto *self, const Call *op) { + if (op->name == name()) { + recursive = true; + } + self->visit_base(op); + }); + } + + return recursive; +} + +bool Function::is_inductive(const string &var) const { + if (!has_pure_definition()) { + return false; + } + + int pos = -1; + for (size_t i = 0; i < definition().args().size(); i++) { + if (const auto &v = definition().args()[i].as()) { + if (v->name == var) { + pos = i; + } + } + } + + if (pos == -1) { + return false; + } + + bool recursive = false; + bool inductive_in_var = false; + for (const Expr &e : definition().values()) { + visit_with(e, [&](auto *self, const Call *op) { + if (op->name == name()) { + recursive = true; + if (const auto &v = op->args[pos].as()) { + if (v->name != var) { + inductive_in_var = true; + } + } else { + inductive_in_var = true; + } + } + self->visit_base(op); + }); + } + + return inductive_in_var; +} + bool Function::can_be_inlined() const { - return is_pure() && definition().specializations().empty(); + return is_pure() && definition().specializations().empty() && !is_inductive(); } bool Function::has_update_definition() const { diff --git a/src/Function.h b/src/Function.h index 55800f3457e5..16c8bb221825 100644 --- a/src/Function.h +++ b/src/Function.h @@ -187,6 +187,12 @@ class Function { !has_extern_definition()); } + /** Does this function have an inductive pure definition? */ + bool is_inductive() const; + + /** Is this function inductive in the given variable? */ + bool is_inductive(const std::string &var) const; + /** Is it legal to inline this function? */ bool can_be_inlined() const; diff --git a/src/IR.cpp b/src/IR.cpp index ae0e38a36251..86d322ce766f 100644 --- a/src/IR.cpp +++ b/src/IR.cpp @@ -599,10 +599,15 @@ Expr Call::make(const Function &func, const std::vector &args, int idx) { internal_assert(idx >= 0 && idx < func.outputs()) << "Value index out of range in call to halide function\n"; - internal_assert(func.has_pure_definition() || func.has_extern_definition()) - << "Call to undefined halide function\n"; - return make(func.output_types()[(size_t)idx], func.name(), args, Halide, - func.get_contents(), idx, Buffer<>(), Parameter()); + /*internal_assert(func.has_pure_definition() || func.has_extern_definition()) + << "Call to undefined halide function\n"; */ + if (func.has_pure_definition() || func.has_extern_definition()) { + return make(func.output_types()[(size_t)idx], func.name(), args, Halide, + func.get_contents(), idx, Buffer<>(), Parameter()); + } else { + return make(func.required_types()[(size_t)idx], func.name(), args, Halide, + func.get_contents(), idx, Buffer<>(), Parameter()); + } } namespace { diff --git a/src/IROperator.cpp b/src/IROperator.cpp index 0b9e4c202ec1..d847ff39d0ac 100644 --- a/src/IROperator.cpp +++ b/src/IROperator.cpp @@ -370,6 +370,11 @@ Expr make_const_helper(Type t, T val) { return UIntImm::make(t, (uint64_t)val); } else if (t.is_float()) { return FloatImm::make(t, (double)val); + } else if (t.is_unknown()) { + user_error << "Can't make a constant of unknown type.\n" + << "This is likely caused by a failure of type inference inside an inductive function definition.\n" + << "If you are trying to create an inductive function, you must define its type explicitly.\n"; + return Expr(); } else { internal_error << "Can't make a constant of type " << t << "\n"; return Expr(); @@ -689,6 +694,16 @@ void match_types(Expr &a, Expr &b) { return; } + if (a.type().is_unknown() && !b.type().is_unknown()) { + b = cast(a.type(), b); + return; + } + + if (b.type().is_unknown() && !a.type().is_unknown()) { + a = cast(b.type(), a); + return; + } + user_assert(!a.type().is_handle() && !b.type().is_handle()) << "Can't do arithmetic on opaque pointer types: " << a << ", " << b << "\n"; @@ -1507,12 +1522,47 @@ Expr saturating_cast(Type t, Expr e) { return Call::make(t, Call::saturating_cast, {std::move(e)}, Call::PureIntrinsic); } +Expr declare_type(Type t, const Expr &e) { + // TODO: This may be called on unsanitized exprs. May need CSE. + internal_assert(e.type().is_unknown()); + if (const Call *op = e.as()) { + internal_assert(op->call_type == Call::Halide); + return Call::make(t, op->name, op->args, op->call_type, + op->func, op->value_index, op->image, op->param); + } else if (const Add *op = e.as()) { + return Add::make(declare_type(t, op->a), declare_type(t, op->b)); + } else if (const Sub *op = e.as()) { + return Sub::make(declare_type(t, op->a), declare_type(t, op->b)); + } else if (const Mul *op = e.as()) { + return Mul::make(declare_type(t, op->a), declare_type(t, op->b)); + } else if (const Div *op = e.as
()) { + return Div::make(declare_type(t, op->a), declare_type(t, op->b)); + } else if (const Min *op = e.as()) { + return Min::make(declare_type(t, op->a), declare_type(t, op->b)); + } else if (const Max *op = e.as()) { + return Max::make(declare_type(t, op->a), declare_type(t, op->b)); + } else if (const Cast *op = e.as()) { + // Must be a cast to unknown + return Cast::make(t, op->value); + } else { + user_error << "Can't do top-down type inference on " << e; + } +} + Expr select(Expr condition, Expr true_value, Expr false_value) { if (as_const_int(condition)) { // Why are you doing this? We'll preserve the select node until constant folding for you. condition = cast(Bool(true_value.type().lanes()), std::move(condition)); } + if (true_value.type().is_unknown() && !false_value.type().is_unknown()) { + true_value = declare_type(false_value.type(), true_value); + } + + if (false_value.type().is_unknown() && !true_value.type().is_unknown()) { + false_value = declare_type(true_value.type(), false_value); + } + // Coerce int literals to the type of the other argument if (as_const_int(true_value)) { true_value = cast(false_value.type(), std::move(true_value)); diff --git a/src/IROperator.h b/src/IROperator.h index 797f12870f5d..301177e38a2e 100644 --- a/src/IROperator.h +++ b/src/IROperator.h @@ -389,6 +389,10 @@ inline Expr cast(Expr a) { /** Cast an expression to a new type. */ Expr cast(Type t, Expr a); +/** Declare an Expr with unknown type has a given type. Useful when defining + * Funcs inductively. */ +Expr declare_type(Type t, const Expr &e); + /** Return the sum of two expressions, doing any necessary type * coercion using \ref Internal::match_types */ Expr operator+(Expr a, Expr b); diff --git a/src/IRPrinter.cpp b/src/IRPrinter.cpp index 331e5de658b2..824a24424027 100644 --- a/src/IRPrinter.cpp +++ b/src/IRPrinter.cpp @@ -46,6 +46,8 @@ ostream &operator<<(ostream &out, const Type &type) { case Type::BFloat: out << "bfloat"; break; + case Type::Unknown: + out << "unknown"; } if (!type.is_handle()) { out << type.bits(); @@ -496,6 +498,9 @@ std::ostream &operator<<(std::ostream &out, const DimType &t) { case DimType::PureRVar: out << "PureRVar"; break; + case DimType::InductiveVar: + out << "InductiveVar"; + break; case DimType::ImpureRVar: out << "ImpureRVar"; break; diff --git a/src/Inductive.cpp b/src/Inductive.cpp new file mode 100644 index 000000000000..f6a181c34ce5 --- /dev/null +++ b/src/Inductive.cpp @@ -0,0 +1,148 @@ +#include "Inductive.h" + +#include "Bounds.h" +#include "ConciseCasts.h" +#include "Error.h" +#include "Function.h" +#include "IR.h" +#include "IREquality.h" +#include "IRVisitor.h" +#include "Simplify.h" +#include "Substitute.h" + +namespace Halide { +namespace Internal { + +using std::string; +using std::vector; + +namespace { + +class BaseCaseSolver : public IRVisitor { + using IRVisitor::visit; + const vector &vars; + const string &func; + + const vector &start_box; + + vector condition_intervals; + + Scope bounds; + + int nested_select = 0; + + void visit(const Call *op) override { + if (op->is_intrinsic(Call::if_then_else)) { + // Theoretically there is no need to check op->args[0]. + // Select nodes are only converted to if_then_else when the condition is pure, + // which means the condition cannot have any recursive calls. + // std::cout<<"cond is" << op->args[0]; + op->args[0].accept(this); + + bool left_recurse = false, right_recurse = false; + visit_with(op->args[1], [&](auto *self, const Call *inner_op) { + if (inner_op->name == func) { + left_recurse = true; + } + self->visit_base(inner_op); + }); + visit_with(op->args[2], [&](auto *self, const Call *inner_op) { + if (inner_op->name == func) { + right_recurse = true; + } + self->visit_base(inner_op); + }); + // Again, this check is theoretically unnecessary + user_assert(!(left_recurse && right_recurse)) << "Select node in inductive function " << func << " does not have a base case"; + + nested_select += 1; + vector old_intervals = condition_intervals; + if (left_recurse) { + for (size_t i = 0; i < vars.size(); i++) { + Interval inter = Interval::make_intersection(old_intervals[i], solve_for_outer_interval(simplify(op->args[0]), vars[i])); + condition_intervals[i] = Interval(inter.min, Interval::pos_inf()); + bounds.push(vars[i], condition_intervals[i]); + } + op->args[1].accept(this); + for (const auto &var : vars) { + bounds.pop(var); + } + } + if (right_recurse) { + for (size_t i = 0; i < vars.size(); i++) { + Interval inter = Interval::make_intersection(old_intervals[i], solve_for_outer_interval(simplify(!op->args[0]), vars[i])); + condition_intervals[i] = Interval(inter.min, Interval::pos_inf()); + bounds.push(vars[i], condition_intervals[i]); + } + op->args[2].accept(this); + for (const auto &var : vars) { + bounds.pop(var); + } + } + condition_intervals = old_intervals; + nested_select -= 1; + } else if (op->name == func) { + user_assert(nested_select > 0) << "Function " << func << " contains an inductive function reference outside of a select operation value.\n"; + user_assert(nested_select == 1) << "Function " << func << " contains an inductive function reference inside a nested select operation.\n"; + bool found_inductive = false; + for (size_t position = 0; position < vars.size(); position++) { + const Expr inductive_expr = op->args[position]; + const Expr new_v = Variable::make(inductive_expr.type(), vars[position]); + const Expr gets_lower = simplify(new_v - inductive_expr > 0, bounds); + const Interval i_lower = solve_for_inner_interval(gets_lower, vars[position]); + + Interval new_interval; + if (equal(new_v, inductive_expr)) { + new_interval = start_box[position]; + } else if (i_lower.is_everything()) { + found_inductive = true; + new_interval = Interval(Interval::neg_inf(), start_box[position].max); + } else { + std::ostringstream err; + err << "Inductive variable " << vars[position] << " in inductive function " << func << " is not provably monotonically decreasing outside of the base case."; + user_error << err.str() << "\n"; + } + new_interval = Interval::make_intersection(new_interval, condition_intervals[position]); + Scope i_scope; + i_scope.push(vars[position], new_interval); + result_intervals[position] = Interval::make_union(result_intervals[position], Interval::make_union(new_interval, bounds_of_expr_in_scope(inductive_expr, i_scope))); + } + user_assert(found_inductive) << "Unable to prove in inductive function " << func << " that the inductive step is monotonically decreasing.\n"; + + IRVisitor::visit(op); + + } else { + IRVisitor::visit(op); + } + } + +public: + vector result_intervals; + + BaseCaseSolver(const vector &v, const string &func, const vector &con) + : vars(v), func(func), start_box(con) { + condition_intervals = vector(start_box.size()); + result_intervals = vector(start_box.size(), Interval::nothing()); + } +}; + +} // anonymous namespace + +Box expand_to_include_base_case(const vector &vars, const Expr &RHS, const string &func, const Box &box_required) { + Expr substed = substitute_in_all_lets(RHS); + Box box2 = box_required; + BaseCaseSolver b(vars, func, box_required.bounds); + substed.accept(&b); + for (size_t i = 0; i < vars.size(); i++) { + user_assert(b.result_intervals[i].is_bounded() || b.result_intervals[i].is_empty()) << "Unable to prove that the inductive function " << func << " uses a bounded interval"; + if (!b.result_intervals[i].is_empty()) { + Interval new_interval(min(b.result_intervals[i].min, box_required[i].min), box_required[i].max); + box2[i] = new_interval; + } + } + + return box2; +} + +} // namespace Internal +} // namespace Halide diff --git a/src/Inductive.h b/src/Inductive.h new file mode 100644 index 000000000000..e2f78232c8ef --- /dev/null +++ b/src/Inductive.h @@ -0,0 +1,57 @@ +#ifndef INDUCTIVE_H +#define INDUCTIVE_H + +/** \file + * + * Utilities for processing inductively defined functions. + * + * A simple example of an inductively defined function is + * f(x) = select(x <= 0, input(0), input(x) + f(x - 1)); + * The purpose of inductive functions is to allow execution patterns that are + * impossible with reduction domains. For example, in the following code: + * + * f(x) = select(x <= 0, input(0), input(x) + f(x - 1)); + * g(x) = f(x) / 4; + * f.compute_at(g, x).store_root(); + * + * The resulting program computes a single value of f(x) at each value of g(x), + * thanks to Halide's sliding window optimization. As a result of storage folding, + * only the two most recent values of f(x) are stored at any given time. This is + * impossible if f(x) is defined using a reduction domain, since every value of f(x) + * must be computed and stored before g(x) is computed. + * + * If Halide is unable to perform the sliding window optimization, computing the + * inductive function is generally inefficient. + * + * In inductive functions, any recursive references must be inside a select statement, + * and cannot be inside nested select statements. The inductive arguments in the + * recursive reference must be monotonically decreasing. Currently, only single-valued + * functions are supported. Inductive functions cannot be inlined, and cannot have + * update definitions. + * + * In some cases, the inductive function's type cannot be inferred and must be declared + * explicitly. This occurs when constants appear in operations with a recursive reference. + * For example, in the following code, Halide cannot infer the type of f: + * f(x) = select(x <= 0, 0, f(x - 1) + 1); + * + * To fix this, declare f with an explicit type: + * Func f = Func(Int(32), "f"); + */ + +#include "Bounds.h" +#include "Expr.h" +#include "Interval.h" +#include "Scope.h" +#include "Solve.h" + +namespace Halide { +namespace Internal { + +/** Given an initial box for an inductively defined function, + returns an expanded box that includes the function's non-inductive base case. */ +Box expand_to_include_base_case(const std::vector &vars, const Expr &RHS, const std::string &func, const Box &box_required); + +} // namespace Internal +} // namespace Halide + +#endif diff --git a/src/Monotonic.cpp b/src/Monotonic.cpp index 82934a31de5e..68298975e6ba 100644 --- a/src/Monotonic.cpp +++ b/src/Monotonic.cpp @@ -509,7 +509,7 @@ ConstantInterval derivative_bounds(const Expr &e, const std::string &var, const return ConstantInterval::everything(); } DerivativeBounds m(var, scope); - remove_likelies(remove_promises(e)).accept(&m); + simplify(remove_likelies(remove_promises(e))).accept(&m); return m.result; } diff --git a/src/Schedule.h b/src/Schedule.h index a72c52bbd59f..1906e7231ee1 100644 --- a/src/Schedule.h +++ b/src/Schedule.h @@ -363,6 +363,11 @@ enum class DimType { * definitions you can even redundantly re-evaluate points. */ PureVar = 0, + /** The dim originated from a Var in an inductively defined pure + * definition. InductiveVars cannot be reordered, parallelized, + * or vectorized. */ + InductiveVar, + /** The dim originated from an RVar. You can evaluate a Func at * distinct values of this RVar in any order (including in * parallel) over exactly the interval specified in the @@ -473,6 +478,10 @@ struct Dim { return (dim_type == DimType::PureRVar) || (dim_type == DimType::ImpureRVar); } + bool is_inductive() const { + return dim_type == DimType::InductiveVar; + } + /** Could multiple iterations of this loop happen at the same * time, with reads and writes interleaved in arbitrary ways * according to the memory model of the underlying compiler and diff --git a/src/ScheduleFunctions.cpp b/src/ScheduleFunctions.cpp index a0b4a4c18876..5067c96ebdcd 100644 --- a/src/ScheduleFunctions.cpp +++ b/src/ScheduleFunctions.cpp @@ -1335,7 +1335,7 @@ class InjectFunctionRealization : public IRMutator { // none of the functions in a fused group can be inlined, so this will only // happen when we're lowering a single func. if (provide_name != funcs[0].name() && - !funcs[0].is_pure() && + (!funcs[0].is_pure() || funcs[0].is_inductive()) && funcs[0].schedule().compute_level().is_inlined() && function_is_used_in_stmt(funcs[0], provide_op)) { @@ -2300,7 +2300,7 @@ bool validate_schedule(Function f, const Stmt &s, const Target &target, bool is_ // will get lowered into compute_at innermost and thus can be treated // similarly as a non-inlined Func. if (store_at.is_inlined() && compute_at.is_inlined() && hoist_storage_at.is_inlined()) { - if (f.is_pure()) { + if (f.is_pure() && !f.is_inductive()) { validate_schedule_inlined_function(f); } return true; @@ -2567,6 +2567,9 @@ Stmt schedule_functions(const vector &outputs, const map &env, const Target &target, bool &any_memoized) { + for (const Function &o : outputs) { + user_assert(!o.is_inductive()) << "Function" << o.name() << " is an inductively defined output buffer, which is unsupported.\n"; + } string root_var = LoopLevel::root().lock().to_string(); Stmt s = For::make(root_var, 0, 0, ForType::Serial, Partition::Never, DeviceAPI::Host, Evaluate::make(0)); diff --git a/src/Serialization.cpp b/src/Serialization.cpp index 33de404edffe..475711312543 100644 --- a/src/Serialization.cpp +++ b/src/Serialization.cpp @@ -352,6 +352,8 @@ Serialize::DimType Serializer::serialize_dim_type(const DimType &dim_type) { return Serialize::DimType::PureRVar; case DimType::ImpureRVar: return Serialize::DimType::ImpureRVar; + case DimType::InductiveVar: + return Serialize::DimType::InductiveVar; default: user_error << "Unsupported dim type\n"; return Serialize::DimType::PureVar; diff --git a/src/StorageFolding.cpp b/src/StorageFolding.cpp index 4b4fa8152d4a..3efec47e3a4c 100644 --- a/src/StorageFolding.cpp +++ b/src/StorageFolding.cpp @@ -4,10 +4,12 @@ #include "CSE.h" #include "Debug.h" #include "ExprUsesVar.h" +#include "IREquality.h" #include "IRMutator.h" #include "IROperator.h" #include "IRPrinter.h" #include "Monotonic.h" +#include "Scope.h" #include "Simplify.h" #include "Substitute.h" #include "Util.h" @@ -50,6 +52,122 @@ int count_producers(const Stmt &in, const std::string &name) { return counter.count; } +Stmt scrub_self_reads(const Stmt &s, const string &func) { + return mutate_with(s, [&](auto *self, const Provide *op) { + debug(3) << "Scrubbing self reads in Provide: " << Stmt(op) << " func name: " << func << "\n"; + if (op->name == func) { + vector values; + values.reserve(op->values.size()); + for (const Expr &v : op->values) { + values.push_back(mutate_with(v, [&](auto *self, const Call *op) { + if (op->name == func && op->call_type == Call::Halide) { + return make_zero(op->type); + } + return Call::make(op->type, op->name, self->mutate(op->args), op->call_type, + op->func, op->value_index, op->image, op->param); + })); + } + return Provide::make(op->name, values, op->args, op->predicate); + } + return Provide::make(op->name, self->mutate(op->values), self->mutate(op->args), self->mutate(op->predicate)); + }); +} + +// True if every self-call of func (inside func's own Provide) matches the +// store index in all dimensions except dim, and strictly differs from it in dim. +bool inductive_only_in_dim(const Stmt &body, const string &func, int dim) { + bool single_dim = true; + vector store_args; + bool in_own_provide = false; + visit_with( + body, + [&](auto *self, const Provide *op) { + if (op->name == func) { + store_args = op->args; + in_own_provide = true; + self->visit_base(op); + in_own_provide = false; + } else { + self->visit_base(op); + } + }, + [&](auto *self, const Call *op) { + if (in_own_provide && op->name == func && op->call_type == Call::Halide) { + if ((int)op->args.size() != (int)store_args.size()) { + single_dim = false; + } else { + for (int d = 0; d < (int)op->args.size(); d++) { + bool ok = (d == dim) ? can_prove(op->args[d] != store_args[d]) : can_prove(op->args[d] == store_args[d]); + if (!ok) { + single_dim = false; + } + } + } + } + self->visit_base(op); + }); + return single_dim; +} + +// Verify there is exactly one store to func in the loop body, and that within +// a single iteration of the fold loop that store never writes the same location +// twice. +bool one_store_per_iteration(const Stmt &body, const string &func) { + struct Store { + vector args; + vector> loops; // enclosing inner loops + }; + vector> loops; + vector stores; + visit_with( + body, + [&](auto *self, const For *op) { + loops.emplace_back(op->name, Interval(op->min, op->max)); + op->body.accept(self); + loops.pop_back(); + }, + [&](auto *self, const Provide *op) { + if (op->name == func) { + Store s; + s.args = op->args; + s.loops = loops; + stores.push_back(std::move(s)); + } + self->visit_base(op); + }); + + if (stores.size() != 1) { + return false; + } + + const Store &s = stores[0]; + if (s.loops.empty()) { + return true; + } + + // Rename the inner loop vars to a different iteration and falsify a collision. + Scope bounds; + Expr distinct = const_false(); + vector other = s.args; + for (const auto &lp : s.loops) { + string other_name = lp.first + "$_"; + Expr v = Variable::make(Int(32), lp.first); + Expr ov = Variable::make(Int(32), other_name); + for (Expr &e : other) { + e = graph_substitute(lp.first, ov, e); + } + distinct = distinct || (v != ov); + bounds.push(lp.first, lp.second); + bounds.push(other_name, lp.second); + } + Expr hazard = distinct; + for (size_t d = 0; d < s.args.size(); d++) { + hazard = hazard && (s.args[d] == other[d]); + } + hazard = simplify(common_subexpression_elimination(hazard), bounds); + return is_const_zero(hazard); +} + // Fold the storage of a function in a particular dimension by a particular factor class FoldStorageOfFunction : public IRMutator { string func; @@ -160,6 +278,7 @@ class InjectFoldingCheck : public IRMutator { int dim; bool in_produce; const StorageDim &storage_dim; + bool allow_inplace; using IRMutator::visit; Stmt visit(const ProducerConsumer *op) override { @@ -172,7 +291,23 @@ class InjectFoldingCheck : public IRMutator { body = mutate(op->body); } else { // Update valid range based on bounds written to. - Box b = box_provided(body, func.name()); + Box provided = box_provided(body, func.name()); + Box required = box_required(body, func.name()); + Stmt body_no_self = scrub_self_reads(body, func.name()); + Box external_required = box_required(body_no_self, func.name()); + Box external = box_union(provided, external_required); + required.used = Expr(); + external.used = Expr(); + // In-place recurrence: the write aliases the just-read lagged + // slot, so the required footprint can be one element tighter. + if (allow_inplace && dim < (int)required.size() && required[dim].is_bounded()) { + if (storage_dim.fold_forward) { + required[dim].min += 1; + } else { + required[dim].max -= 1; + } + } + Box b = box_union(required, external); Expr old_leading_edge = Load::make(Int(32), head + "_next", 0, Buffer<>(), Parameter(), const_true(), ModulusRemainder()); @@ -386,10 +521,11 @@ class InjectFoldingCheck : public IRMutator { InjectFoldingCheck(Function func, string head, string tail, string loop_var, Expr sema_var, - int dim, const StorageDim &storage_dim) + int dim, const StorageDim &storage_dim, + bool allow_inplace) : func(std::move(func)), head(std::move(head)), tail(std::move(tail)), loop_var(std::move(loop_var)), sema_var(std::move(sema_var)), - dim(dim), storage_dim(storage_dim) { + dim(dim), storage_dim(storage_dim), allow_inplace(allow_inplace) { } }; @@ -535,6 +671,13 @@ class AttemptStorageFoldingOfFunction : public IRMutator { required.used = Expr(); Box box = box_union(provided, required); + // Footprint of func ignoring any recurrence self-read. + // Used to determine whether we can safely alias the write with the self-read slot. + Stmt body_no_self = scrub_self_reads(body, func.name()); + Box external_required = box_required(body_no_self, func.name()); + external_required.used = Expr(); + Box box_external = box_union(provided, external_required); + Expr loop_var = Variable::make(Int(32), op->name); string dynamic_footprint; @@ -597,6 +740,68 @@ class AttemptStorageFoldingOfFunction : public IRMutator { Expr extent = Max::make(extent_initial, extent_steady); extent = simplify(common_subexpression_elimination(extent), bounds); + // Structural safety for aliasing the write with a self-read slot. + // Let the storage folding extent be k + 1. To alias the write, + // we need to ensure that, for each x, all reads and writes of f(x) + // occur after all reads and writes of f(x-k) in the same loop iteration. + // unit_advance checks that the footprint bounds only shift by 1 each iteration. + // The iteration where both f(x) and f(x-k) are accessed must be the + // first iteration where f(x) is within the + // storage-folded footprint, so f(x) must be written to before it is safely read. + // Thus, we only have to check for writes to f(x) instead of both reads and writes. + // can_prove(extent_no_self <= extent - 1, bounds) checks + // that computing the footprint without self-reads makes + // the min value strictly larger than x-k, which ensures that f(x-k) is + // not written to in the same iteration as f(x), and all reads of f(x-k) + // are self-reads used to compute values of f. It is thus sufficient to + // check that f(x-k) is only used to compute a single value of f(x), and + // no additional values of f are computed. We allow f to have additional + // dimensions other than x, so long as their arguments can be treated as pure variables + // (i.e. the store indices are identical to the recursive self-call indices). + // + // single_write checks that f(x) is the only value that is stored. + // inductive_only_in_dim(op->body, func.name(), dim) checks that the + // recurrence only recurses in the fold dimension, so all other arguments + // of f can be treated as pure variables. + // one_store_per_iteration(op->body, func.name()) + // checks that f(x) is written exactly once per iteration. + // We also check that f is not tuple-valued and has no external definition. + // Storing a tuple value lowers down to multiple store operations, which could + // all use depend on same element of f(x-k). + + bool single_inductive_store = + inductive_only_in_dim(op->body, func.name(), dim) && + one_store_per_iteration(op->body, func.name()); + + bool can_inplace = false; + if (single_inductive_store && dim < (int)box_external.size() && box_external[dim].is_bounded() && func.outputs() == 1 && !func.has_extern_definition()) { + Expr min_e = simplify(common_subexpression_elimination(box_external[dim].min)); + Expr max_e = simplify(common_subexpression_elimination(box_external[dim].max)); + Expr min_e_steady = simplify(substitute(steady_state, const_true(), min_e), steady_bounds); + Expr max_e_steady = simplify(substitute(steady_state, const_true(), max_e), steady_bounds); + Expr min_e_initial = simplify(substitute(steady_state, const_false(), min_e), bounds); + Expr max_e_initial = simplify(substitute(steady_state, const_false(), max_e), bounds); + Expr extent_e_initial = simplify(substitute(loop_var, op->min, max_e_initial - min_e_initial + 1), bounds); + Expr extent_e_steady = simplify(max_e_steady - min_e_steady + 1, steady_bounds); + Expr extent_no_self = simplify(common_subexpression_elimination(Max::make(extent_e_initial, extent_e_steady)), bounds); + + Expr max_step = simplify(substitute(op->name, loop_var + 1, max) - max, bounds); + Expr min_step = simplify(substitute(op->name, loop_var + 1, min) - min, bounds); + bool unit_advance = + (can_prove(max_step == 1, bounds)) || + (can_prove(min_step == -1, bounds)); + bool single_write = + dim < (int)provided.size() && provided[dim].is_bounded() && + can_prove(provided[dim].max <= provided[dim].min, bounds) && + can_prove(provided[dim].max >= max_e, bounds); + + bool extent_shrinks = can_prove(extent_no_self <= extent - 1, bounds); + can_inplace = unit_advance && single_write && extent_shrinks; + } + if (can_inplace) { + extent = simplify(extent - 1); + } + // Find the StorageDim corresponding to dim. const std::vector &storage_dims = func.schedule().storage_dims(); auto storage_dim_i = std::find_if(storage_dims.begin(), storage_dims.end(), @@ -691,7 +896,8 @@ class AttemptStorageFoldingOfFunction : public IRMutator { op->name, sema_var, dim, - storage_dim)(body); + storage_dim, + can_inplace)(body); if (storage_dim.fold_forward) { can_fold_forwards = true; diff --git a/src/Type.h b/src/Type.h index 174c53de4961..eaa3c08ea509 100644 --- a/src/Type.h +++ b/src/Type.h @@ -291,6 +291,7 @@ struct Type { static constexpr halide_type_code_t Float = halide_type_float; static constexpr halide_type_code_t BFloat = halide_type_bfloat; static constexpr halide_type_code_t Handle = halide_type_handle; + static constexpr halide_type_code_t Unknown = halide_type_unknown; // @} /** The number of bytes required to store a single scalar value of this type. Ignores vector lanes. */ @@ -456,6 +457,12 @@ struct Type { return code() == Handle; } + /** Is this type a floating point type (float or double). */ + HALIDE_ALWAYS_INLINE + bool is_unknown() const { + return code() == Unknown; + } + // Returns true iff type is a signed integral type where overflow is defined. HALIDE_ALWAYS_INLINE bool can_overflow_int() const { @@ -569,6 +576,11 @@ inline Type Handle(int lanes = 1, const halide_handle_cplusplus_type *handle_typ return Type(Type::Handle, 64, lanes, handle_type); } +/** Construct an unknown type */ +inline Type Unknown() { + return Type(Type::Unknown, 0, 1); +} + /** Construct the halide equivalent of a C type */ template inline Type type_of() { diff --git a/src/halide_ir.fbs b/src/halide_ir.fbs index 7f3492684743..480dab36a8d4 100644 --- a/src/halide_ir.fbs +++ b/src/halide_ir.fbs @@ -562,6 +562,7 @@ table Split { enum DimType: ubyte { PureVar, + InductiveVar, PureRVar, ImpureRVar, } diff --git a/src/runtime/HalideRuntime.h b/src/runtime/HalideRuntime.h index fe250a12430d..b9afc52683e0 100644 --- a/src/runtime/HalideRuntime.h +++ b/src/runtime/HalideRuntime.h @@ -493,11 +493,12 @@ typedef enum halide_type_code_t : uint8_t #endif { - halide_type_int = 0, ///< signed integers - halide_type_uint = 1, ///< unsigned integers - halide_type_float = 2, ///< IEEE floating point numbers - halide_type_handle = 3, ///< opaque pointer type (void *) - halide_type_bfloat = 4, ///< floating point numbers in the bfloat format + halide_type_int = 0, ///< signed integers + halide_type_uint = 1, ///< unsigned integers + halide_type_float = 2, ///< IEEE floating point numbers + halide_type_handle = 3, ///< opaque pointer type (void *) + halide_type_bfloat = 4, ///< floating point numbers in the bfloat format + halide_type_unknown = 5, ///< an expression of unknown type, to be determined later } halide_type_code_t; // Note that while __attribute__ can go before or after the declaration, diff --git a/test/correctness/CMakeLists.txt b/test/correctness/CMakeLists.txt index 821a62525a2d..17852ad0cf94 100644 --- a/test/correctness/CMakeLists.txt +++ b/test/correctness/CMakeLists.txt @@ -185,6 +185,8 @@ tests( implicit_pure_def_with_rvar_args.cpp in_place.cpp indexing_access_undef.cpp + inductive.cpp + inductive_folding.cpp infer_arguments.cpp inline_reduction.cpp inlined_generator.cpp diff --git a/test/correctness/inductive.cpp b/test/correctness/inductive.cpp new file mode 100644 index 000000000000..a5f0362f2e9e --- /dev/null +++ b/test/correctness/inductive.cpp @@ -0,0 +1,299 @@ +#include "Halide.h" +#include "check_call_graphs.h" +#include "test_sharding.h" + +#include +#include + +namespace { + +using std::map; +using std::string; + +using namespace Halide; +using namespace Halide::Internal; + +int simple_inductive_test() { + Func g(Int(32), "g"), h("h"); + Var x("x"), y("y"); + + g(x, y) = select(x <= 0, 0, likely(g(max(0, x - 1), y) + x + y)); + + h(x, y) = g(x + 5, y) / 4; + + g.compute_at(h, x).store_at(h, y); + + h.bound(x, 0, 600).bound(y, 0, 5); + + Buffer im = h.realize({600, 5}); + auto func = [](int x, int y) { + return (y * (x + 5) + (x + 5) * (x + 6) / 2) / 4; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int reorder_test() { + Func g("g"), h("h"); + Var x("x"), y("y"); + + Var xi("xi"), xii("xii"), xo("xo"); + + g(x, y) = select(x <= 0, 0, g(max(0, x - 1), y) + x + y); + + h(x, y) = g(x + 5, y) / 4; + h.split(x, xo, xi, 24).reorder(xi, y, xo); + + g.compute_at(h, xo).store_root(); + + g.split(x, xi, xii, 5).reorder(xii, y, xi).vectorize(y, 8); + + Buffer im = h.realize({80, 80}); + auto func = [](int x, int y) { + return (y * (x + 5) + (x + 5) * (x + 6) / 2) / 4; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int summed_area_table() { + Func f("f"), g("g"), h("h"); + Var x("x"), y("y"); + f(x, y) = x + y; + g(x, y) = f(x, y) + select(x <= 0, 0, g(x - 1, y)) + select(y <= 0, 0, g(x, y - 1)) - select(x <= 0 || y <= 0, 0, g(x - 1, y - 1)); + h(x, y) = g(x, y) / 8; + g.compute_at(h, x).store_root(); + + Buffer im = h.realize({80, 80}); + auto func = [](int x, int y) { + return (x * (x + 1) / 2 * (y + 1) + y * (y + 1) / 2 * (x + 1)) / 8; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int large_baseline() { + Func g("g"), h("h"); + Var x("x"), y("y"); + + g(x, y) = select(x <= 8, (y * x + x * (x + 1) / 2) - 1, g(x - 1, y) + x + y); + h(x, y) = g(x + 5, y) / 4; + + g.compute_at(h, x).store_at(h, y); + + Buffer im = h.realize({80, 80}); + auto func = [](int x, int y) { + return (y * (x + 5) + (x + 5) * (x + 6) / 2 - 1) / 4; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int fibonacci() { + Func g("g"), h("h"); + Var x("x"), y("y"); + + g(x, y) = select(x <= 1, 1, g(x - 1, y) + g(x - 2, y)); + h(x, y) = g(x, y); + + h.bound(x, 0, 80); + Buffer im = h.realize({80, 80}); + auto func = [](int x, int y) { + int a = 1; + int b = 1; + for (int i = 2; i <= x; i++) { + int c = a + b; + b = a; + a = c; + } + return a; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int sum_2d_test() { + Func f("f"), g("g"), h("h"); + Var x("x"), y("y"); + f(x, y) = select(x <= 0, 0, x + f(x - 1, y)); + g(x, y) = select(y <= 0, f(x, 0), f(x, y) + g(x, y - 1)); + h(x, y) = g(x, y); + h.bound(x, 0, 80).bound(y, 0, 80).vectorize(x, 8); + g.compute_at(h, x).store_root().vectorize(x, 8); + f.compute_at(h, x).store_root(); + Buffer im = h.realize({80, 80}); + auto func = [](int x, int y) { + int result = 0; + for (int a = 0; a <= x; a++) { + for (int b = 0; b <= y; b++) { + result += a; + } + } + return result; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int sum_1d_test() { + Func f("f"), g("g"), h("h"); + Var x("x"), y("y"); + f(x, y) = x + y; + f(x, y) += x; + g(x, y) = select(y <= 0, f(x, 0), f(x, y) + g(x, y - 1)); + h(x, y) = g(x, y); + h.bound(x, 0, 80).bound(y, 0, 80); + f.compute_at(h, x); + Buffer im = h.realize({80, 80}); + auto func = [](int x, int y) { + int result = 0; + for (int a = 0; a <= y; a++) { + result += 2 * x + a; + } + return result; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int multi_baseline_test() { + Func f("f"), g("g"), h("h"); + Var x("x"), y("y"); + f(x, y) = x + y; + f(x, y) += x; + g(x, y) = select(y <= 0, f(x, 0), f(x, y) + g(x, y - 1)) + select(y <= 3, f(x, 0), f(x, y) + g(x, y - 1)); + h(x, y) = g(x, y); + h.bound(x, 0, 80).bound(y, 0, 20); + f.compute_at(h, x); + Buffer im = h.realize({80, 20}); + auto func = [](int x, int y) { + std::vector result; + + for (int a = 0; a <= y; a++) { + if (a <= 0) { + result.emplace_back(4 * x); + } else if (a <= 3) { + result.emplace_back(2 * x + (2 * x + a) + result[a - 1]); + } else { + result.emplace_back(2 * x + a + result[a - 1] + (2 * x + a) + result[a - 1]); + } + } + return result[y]; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int type_declare_test() { + Func g = Func(Int(32), "g"); + Func h("h"); + Var x("x"), y("y"); + + g(x, y) = select(x <= 0, 0, 1 + g(max(0, x - 1), y) + x + 2); + + h(x, y) = g(x + 5, y) / 4; + + g.compute_at(h, x).store_at(h, y); + + Buffer im = h.realize({600, 5}); + auto func = [](int x, int y) { + return (3 * (x + 5) + (x + 5) * (x + 6) / 2) / 4; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int tuple_test() { + Func g = Func(std::vector{Int(32), Int(32)}, "g"); + Func h("h"); + Var x("x"), y("y"); + + g(x, y) = Tuple(select(x <= 0, 0, g(max(0, x - 1), y)[0] + x + y), x - y); + h(x, y) = g(10, y)[0] / 4 + g(10, y)[1] + x; + + g.compute_root(); + + Buffer im = h.realize({10, 10}); + auto func = [](int x, int y) { + return (y * (5 + 5) + (5 + 5) * (5 + 6) / 2) / 4 + 10 - y + x; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +int tuple_test_2() { + Func g = Func(std::vector{Int(32), Int(32)}, "g"); + Func h("h"); + Var x("x"), y("y"); + + g(x, y) = Tuple(select(x <= 0, 0, g(max(0, x - 1), y)[0] + x + y), select(y <= 0, 0, g(x, max(0, y - 1))[1] + x + y)); + h(x, y) = g(x, y)[0] + g(x, y)[1]; + + g.compute_root(); + + Buffer im = h.realize({10, 10}); + auto func = [](int x, int y) { + return y * x + x * (x + 1) / 2 + x * y + y * (y + 1) / 2; + }; + if (check_image(im, func)) { + return 1; + } + return 0; +} + +} // namespace + +int main(int argc, char **argv) { + struct Task { + std::string desc; + std::function fn; + }; + + std::vector tasks = { + {"simple inductive test", simple_inductive_test}, + {"reordering test", reorder_test}, + {"summed area table test", summed_area_table}, + {"large baseline test", large_baseline}, + {"fibonacci test", fibonacci}, + {"2d sum test", sum_2d_test}, + {"1d sum test", sum_1d_test}, + {"multi-baseline test", multi_baseline_test}, + {"type declaration test", type_declare_test}, + {"tuple test", tuple_test}, + {"tuple test 2", tuple_test_2}, + }; + + using Sharder = Halide::Internal::Test::Sharder; + Sharder sharder; + for (size_t t = 0; t < tasks.size(); t++) { + if (!sharder.should_run(t)) continue; + const auto &task = tasks.at(t); + std::cout << task.desc << "\n"; + if (task.fn() != 0) { + return 1; + } + } + + printf("Success!\n"); + return 0; +} diff --git a/test/correctness/inductive_folding.cpp b/test/correctness/inductive_folding.cpp new file mode 100644 index 000000000000..4f0de0dde7dd --- /dev/null +++ b/test/correctness/inductive_folding.cpp @@ -0,0 +1,86 @@ +#include "Halide.h" +#include "check_call_graphs.h" +#include "test_sharding.h" + +#include +#include +#include + +namespace { + +using namespace Halide; + +int fib_fold2() { + Func f(Int(32), "f"), g("g"); + Var x("x"), y("y"); + f(x, y) = select(x <= 1, x + y, likely(f(x - 1, y) + f(x - 2, y))); + g(x, y) = f(x, y); + f.compute_at(g, x).store_root().fold_storage(x, 2); + g.bound(x, 0, 30).bound(y, 0, 8); + + Buffer im = g.realize({30, 8}); + return check_image(im, [](int x, int y) { + if (x <= 1) return x + y; + int a = y, b = 1 + y; + for (int i = 2; i <= x; i++) { + int c = b + a; + a = b; + b = c; + } + return b; + }); +} + +int multi_inner_injective_fold1() { + Func f(Int(32), "f"), g("g"); + Var x("x"), y("y"), z("z"); + f(x, y, z) = select(x <= 0, y + z, likely(f(x - 1, y, z) + 1)); + g(x, y, z) = f(x, y, z); + g.reorder(z, y, x); + f.compute_at(g, x).store_root().fold_storage(x, 1); + g.bound(x, 0, 16).bound(y, 0, 4).bound(z, 0, 4); + + Buffer im = g.realize({16, 4, 4}); + return check_image(im, [](int x, int y, int z) { return x + y + z; }); +} + +int consumer_strided_nonfold_fold1() { + Func f(Int(32), "f"), g("g"); + Var x("x"), y("y"); + f(x, y) = select(x <= 0, y, likely(f(x - 1, y) + 1)); // f(x,y) = x + y + g(x, y) = f(x, 2 * y); + f.compute_at(g, x).store_root().fold_storage(x, 1); + g.bound(x, 0, 64).bound(y, 0, 8); + + Buffer im = g.realize({64, 8}); + return check_image(im, [](int x, int y) { return x + 2 * y; }); +} + +} // namespace + +int main(int argc, char **argv) { + struct Task { + std::string desc; + std::function fn; + }; + + std::vector tasks = { + {"multi-lag (x-1, x-2), fold_storage 2", fib_fold2}, + {"multi-inner-dim recurrence, fold_storage 1", multi_inner_injective_fold1}, + {"strided non-fold consumer access, fold_storage 1", consumer_strided_nonfold_fold1}, + }; + + using Sharder = Halide::Internal::Test::Sharder; + Sharder sharder; + for (size_t t = 0; t < tasks.size(); t++) { + if (!sharder.should_run(t)) continue; + const auto &task = tasks.at(t); + std::cout << task.desc << "\n"; + if (task.fn() != 0) { + return 1; + } + } + + printf("Success!\n"); + return 0; +} diff --git a/test/error/CMakeLists.txt b/test/error/CMakeLists.txt index 0d18a7a87a3a..d9ee649f3aa9 100644 --- a/test/error/CMakeLists.txt +++ b/test/error/CMakeLists.txt @@ -75,6 +75,25 @@ tests( implicit_args.cpp impossible_constraints.cpp incomplete_target.cpp + inductive_2d_arbitrary.cpp + inductive_cond_nested.cpp + inductive_cond_nested_select.cpp + inductive_cond_self_reference.cpp + inductive_diagonal_fold.cpp + inductive_external_lag_fold.cpp + inductive_loop_1.cpp + inductive_loop_2.cpp + inductive_loop_3.cpp + inductive_nested_select.cpp + inductive_no_select.cpp + inductive_redundant_z_fold.cpp + inductive_reorder.cpp + inductive_small_fold.cpp + inductive_tuple_fold.cpp + inductive_tuple_to_expr.cpp + inductive_update.cpp + inductive_var_swap.cpp + inductive_vectorize.cpp init_def_should_be_all_vars.cpp input_buffer_func_name_collision.cpp input_generator_buffer_func_name_collision.cpp diff --git a/test/error/inductive_2d_arbitrary.cpp b/test/error/inductive_2d_arbitrary.cpp new file mode 100644 index 000000000000..41ca9976fc14 --- /dev/null +++ b/test/error/inductive_2d_arbitrary.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"), y("y"); + + f(x, y) = select(x < 2 || y < 2 || y > 6, 0, f(x - 1, y + 1)) + select(x < 2 || x > 6 || y < 2, 0, f(x + 1, y - 1)); + g(x, y) = f(x, y) * 2; + + g.realize({10, 10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_cond_nested.cpp b/test/error/inductive_cond_nested.cpp new file mode 100644 index 000000000000..b16ecf982df9 --- /dev/null +++ b/test/error/inductive_cond_nested.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x < max(2, abs(select(x < 0, f(x - 1), 5))), 0, f(x - 1) + x); + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_cond_nested_select.cpp b/test/error/inductive_cond_nested_select.cpp new file mode 100644 index 000000000000..088d861de925 --- /dev/null +++ b/test/error/inductive_cond_nested_select.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f(Int(32), "f"), g("g"); + + Var x("x"); + + f(x) = select(x < 0, 0, f(x - 1) + select(x < f(x + 1), 0, 1)); + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_cond_self_reference.cpp b/test/error/inductive_cond_self_reference.cpp new file mode 100644 index 000000000000..1999f7acb3b4 --- /dev/null +++ b/test/error/inductive_cond_self_reference.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x < f(x - 1), 0, f(x - 1) + x); + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_diagonal_fold.cpp b/test/error/inductive_diagonal_fold.cpp new file mode 100644 index 000000000000..eb83c020ae80 --- /dev/null +++ b/test/error/inductive_diagonal_fold.cpp @@ -0,0 +1,19 @@ +#include "Halide.h" +#include + +using namespace Halide; + +// Illegal to fold a dimension of a 2-d recurrence to a fold factor of 1, +// since it would create a read-after-write hazard. +int main(int argc, char **argv) { + Func f(Int(32), "f"), g("g"); + Var x("x"), y("y"); + f(x, y) = select(x <= 0 || y <= 0, x + y, likely(f(x - 1, y - 1) + 1)); + g(x, y) = f(x, y); + f.compute_at(g, x).store_root().fold_storage(y, 1); + g.bound(x, 0, 64).bound(y, 0, 8); + Buffer im = g.realize({64, 8}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_external_lag_fold.cpp b/test/error/inductive_external_lag_fold.cpp new file mode 100644 index 000000000000..04c1335e035d --- /dev/null +++ b/test/error/inductive_external_lag_fold.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +// Both f(x) and f(x-1) must be live to compute g, so a fold factor of 1 is unsafe. +int main(int argc, char **argv) { + Func f(Int(32), "f"), g("g"); + Var x("x"), y("y"); + f(x, y) = select(x <= 0, y, likely(f(x - 1, y) + 1)); + g(x, y) = f(x, y) + f(max(x - 1, 0), y); + f.compute_at(g, x).store_root().fold_storage(x, 1); + g.bound(x, 0, 64).bound(y, 0, 8); + Buffer im = g.realize({64, 8}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_loop_1.cpp b/test/error/inductive_loop_1.cpp new file mode 100644 index 000000000000..cb894256a355 --- /dev/null +++ b/test/error/inductive_loop_1.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x < 1, 0, f(x)); + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_loop_2.cpp b/test/error/inductive_loop_2.cpp new file mode 100644 index 000000000000..f6086e239eef --- /dev/null +++ b/test/error/inductive_loop_2.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x < 2, 0, f(x - 1) + f(x) + f(x - 2)); + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_loop_3.cpp b/test/error/inductive_loop_3.cpp new file mode 100644 index 000000000000..c281480d5fa7 --- /dev/null +++ b/test/error/inductive_loop_3.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x < 2, 0, f(x - 1) + f(min(2, x)) + f(x - 2)); + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_nested_select.cpp b/test/error/inductive_nested_select.cpp new file mode 100644 index 000000000000..c9abf663008f --- /dev/null +++ b/test/error/inductive_nested_select.cpp @@ -0,0 +1,19 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + // Nested select operations are currently unsupported. + f(x) = select(x < 1, 0, select(x < 3, 1, f(x - 1))); + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_no_select.cpp b/test/error/inductive_no_select.cpp new file mode 100644 index 000000000000..5fdc3f430232 --- /dev/null +++ b/test/error/inductive_no_select.cpp @@ -0,0 +1,19 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = cast(x + f(x - 1)); + + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_redundant_z_fold.cpp b/test/error/inductive_redundant_z_fold.cpp new file mode 100644 index 000000000000..5b779e77e958 --- /dev/null +++ b/test/error/inductive_redundant_z_fold.cpp @@ -0,0 +1,21 @@ +#include "Halide.h" +#include + +using namespace Halide; + +// Redundantly recomputes f(x, y) for every z, so f(x - 1, y) +// cannot safely be overwritten when computing f(x, y). +// A fold factor of 1 for x is unsafe. +int main(int argc, char **argv) { + Func f(Int(32), "f"), h("h"); + Var x("x"), y("y"), z("z"); + f(x, y) = select(x <= 0, y, likely(f(x - 1, y) + 1)); + h(x, y, z) = f(x, y); + h.reorder(z, y, x); // x outer, then y, z inner + f.compute_at(h, z).store_root().fold_storage(x, 1); + h.bound(x, 0, 64).bound(y, 0, 8).bound(z, 0, 4); + Buffer im = h.realize({64, 8, 4}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_reorder.cpp b/test/error/inductive_reorder.cpp new file mode 100644 index 000000000000..01dbe642645b --- /dev/null +++ b/test/error/inductive_reorder.cpp @@ -0,0 +1,21 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"), xi("xi"), xo("xo"); + + f(x) = select(x < 1, 0, x + f(x - 1)); + f.split(x, xo, xi, 8); + f.reorder(xo, xi); + + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_small_fold.cpp b/test/error/inductive_small_fold.cpp new file mode 100644 index 000000000000..e5573de2cb47 --- /dev/null +++ b/test/error/inductive_small_fold.cpp @@ -0,0 +1,19 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x <= 0, 0, f(x - 1) + f(x - 2) + x); + g(x) = f(x) * 2; + + f.compute_at(g, x).store_root().fold_storage(x, 1); + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_tuple_fold.cpp b/test/error/inductive_tuple_fold.cpp new file mode 100644 index 000000000000..512e0e1d0d60 --- /dev/null +++ b/test/error/inductive_tuple_fold.cpp @@ -0,0 +1,21 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f(std::vector{Int(32), Int(32)}, "f"), g("g"); + + Var x("x"); + + // We can't fold the storage to size 1 because Halide could overwrite the first Tuple element before reading it + // to compute the second Tuple element. + f(x) = select(x <= 0, Tuple(0, 0), Tuple(f(x - 1)[0] + 1, f(x - 1)[1] + f(x - 1)[0])); + g(x) = f(x)[1] * 2; + + f.compute_at(g, x).store_root().fold_storage(x, 1); + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_tuple_to_expr.cpp b/test/error/inductive_tuple_to_expr.cpp new file mode 100644 index 000000000000..369595f7a6da --- /dev/null +++ b/test/error/inductive_tuple_to_expr.cpp @@ -0,0 +1,18 @@ +#include "Halide.h" + +using namespace Halide; + +int main(int argc, char **argv) { + Func g = Func(std::vector{Int(32), Int(32)}, "g"); + Func h("h"); + Var x("x"), y("y"); + + g(x, y) = Tuple(select(x <= 0, 0, g(max(0, x - 1), y) + x + y), select(y <= 0, 0, g(x, max(0, y - 1)) + x + y)); + + h(x, y) = g(x, y)[0] / 2; + + h.realize({10, 10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_update.cpp b/test/error/inductive_update.cpp new file mode 100644 index 000000000000..cc4312dc0353 --- /dev/null +++ b/test/error/inductive_update.cpp @@ -0,0 +1,20 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x < 1, 0, x + f(x - 1)); + f(x) += 1; + + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_var_swap.cpp b/test/error/inductive_var_swap.cpp new file mode 100644 index 000000000000..4a8453eb0901 --- /dev/null +++ b/test/error/inductive_var_swap.cpp @@ -0,0 +1,19 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"), y("y"); + + f(x, y) = select(x < 1, 0, x + f(y - 1, x - 1)); + + g(x, y) = f(x, y) * 2; + + g.realize({10, 10}); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/inductive_vectorize.cpp b/test/error/inductive_vectorize.cpp new file mode 100644 index 000000000000..bc6dd1e4bc95 --- /dev/null +++ b/test/error/inductive_vectorize.cpp @@ -0,0 +1,20 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Func f("f"), g("g"); + + Var x("x"); + + f(x) = select(x < 1, 0, x + f(x - 1)); + f.vectorize(x, 8); + + g(x) = f(x) * 2; + + g.realize({10}); + + printf("Success!\n"); + return 0; +}