diff --git a/Makefile b/Makefile index 44b98aa9b998..5435d670c42c 100644 --- a/Makefile +++ b/Makefile @@ -464,6 +464,7 @@ SOURCE_FILES = \ Buffer.cpp \ Callable.cpp \ CanonicalizeGPUVars.cpp \ + CheckGPUCrossTalk.cpp \ ClampUnsafeAccesses.cpp \ Closure.cpp \ CodeGen_ARM.cpp \ @@ -667,6 +668,7 @@ HEADER_FILES = \ Buffer.h \ Callable.h \ CanonicalizeGPUVars.h \ + CheckGPUCrossTalk.h \ ClampUnsafeAccesses.h \ Closure.h \ CodeGen_C.h \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7727588fe764..aa86aa833a49 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -74,6 +74,7 @@ target_sources( Buffer.h Callable.h CanonicalizeGPUVars.h + CheckGPUCrossTalk.h ClampUnsafeAccesses.h Closure.h CodeGen_C.h @@ -257,6 +258,7 @@ target_sources( Buffer.cpp Callable.cpp CanonicalizeGPUVars.cpp + CheckGPUCrossTalk.cpp ClampUnsafeAccesses.cpp Closure.cpp CodeGen_ARM.cpp diff --git a/src/CheckGPUCrossTalk.cpp b/src/CheckGPUCrossTalk.cpp new file mode 100644 index 000000000000..73f2a4ecfb6f --- /dev/null +++ b/src/CheckGPUCrossTalk.cpp @@ -0,0 +1,310 @@ +#include "CheckGPUCrossTalk.h" + +#include "Bounds.h" +#include "CanonicalizeGPUVars.h" +#include "IR.h" +#include "IROperator.h" +#include "IRPrinter.h" +#include "IRVisitor.h" +#include "Simplify.h" +#include "Substitute.h" + +#include + +namespace Halide { +namespace Internal { + +using std::string; +using std::vector; + +namespace { + +// One loop over GPU threads. The producer and the consumer of an allocation +// have their own loops over the same threads, which may not even have the same +// bounds, so an access is only meaningful alongside the loops around it. +struct ThreadLoop { + string name; + Expr min, extent; +}; + +// An access to the allocation being checked, by dimension. Accesses are +// recorded in the order they appear, so an earlier one in the list is one that +// has already happened. +struct Access { + // As written, for error messages. Left in terms of whatever variables the + // schedule named, which is what the user will recognize. + vector args; + // In terms of the loops the fused loops over threads will use, so that two + // accesses can be compared. + vector canonical_args; + // How many loops over threads this sits in. + int thread_depth; + bool is_store; +}; + +class FindAccesses : public IRVisitor { + using IRVisitor::visit; + + // Rewrite an expr into the terms two accesses can be compared in. An index + // is usually written using let-bound variables, and the producer and the + // consumer name theirs differently, so put the definitions back around it. + // Then swap each loop over threads for the fused loop it will become: + // counting inwards, the nth loop around the expr is the nth thread + // dimension, whatever it is called, and its min is folded in, because the + // fused loop starts at zero. + Expr canonicalize(const Expr &e) const { + Expr result = rewrap_used_lets(e, lets); + for (size_t i = 0; i < thread_loops.size() && i < 3; i++) { + const ThreadLoop &t = thread_loops[thread_loops.size() - 1 - i]; + Expr v = Variable::make(Int(32), gpu_thread_name((int)i)) + t.min; + result = simplify(substitute(t.name, v, result)); + } + return result; + } + + void visit(const For *op) override { + // Only a loop over threads separates one thread's copy of the + // allocation from another's. A loop over lanes does not: the lanes of a + // warp can reach each other's registers, which is what LowerWarpShuffles + // is for, so one is just another loop a single thread runs. + if (op->for_type == ForType::GPUThread) { + thread_loops.push_back({op->name, op->min, op->extent()}); + IRVisitor::visit(op); + thread_loops.pop_back(); + } else { + // The loops a thread runs inside its own part of the allocation + // bound how far its accesses reach, which is what says the parts + // do not overlap. A loop that starts at the thread's own part of + // the allocation has bounds that speak of the thread, so they get + // the same treatment as the accesses themselves. + Expr min = canonicalize(op->min), max = canonicalize(op->max); + loop_bounds.emplace_back(op->name, Interval(min, max)); + IRVisitor::visit(op); + } + } + + void visit(const LetStmt *op) override { + op->value.accept(this); + lets.emplace_back(op->name, op->value); + op->body.accept(this); + lets.pop_back(); + } + + void visit(const Let *op) override { + op->value.accept(this); + lets.emplace_back(op->name, op->value); + op->body.accept(this); + lets.pop_back(); + } + + // Record an access, in terms of both the loops it was written with and the + // loops over threads it will end up in. + void record(const vector &args, bool is_store) { + vector canonical; + canonical.reserve(args.size()); + for (const Expr &e : args) { + canonical.push_back(canonicalize(e)); + } + // The nth loop counting inwards from this access is the nth thread + // dimension, so that is where its extent belongs. + int depth = (int)thread_loops.size(); + for (int i = 0; i < depth && i < 3; i++) { + const Expr &e = thread_loops[depth - 1 - i].extent; + thread_extents[i] = thread_extents[i].defined() ? + simplify(max(thread_extents[i], e)) : + e; + } + accesses.push_back({args, canonical, depth, is_store}); + } + + void visit(const Provide *op) override { + // The values are read before the store happens, which matters for an + // update definition, where the value reads the site being stored to. + IRVisitor::visit(op); + if (op->name == func) { + record(op->args, true); + } + } + + void visit(const Call *op) override { + if (op->name == func && op->call_type == Call::Halide) { + record(op->args, false); + } + IRVisitor::visit(op); + } + + const string &func; + vector thread_loops; + vector> lets; + +public: + vector accesses; + vector> loop_bounds; + // The most threads there are in each dimension, if that is known. + Expr thread_extents[3]; + + FindAccesses(const string &func) + : func(func) { + } +}; + +string name_and_args(const string &name, const vector &args) { + std::ostringstream s; + s << name << "("; + for (size_t i = 0; i < args.size(); i++) { + s << (i ? ", " : "") << args[i]; + } + s << ")"; + return s.str(); +} + +class CheckCrossTalk : public IRVisitor { + using IRVisitor::visit; + + bool in_threads = false; + + void visit(const For *op) override { + // An allocation inside a loop over threads or lanes already belongs to + // whoever runs that loop, so there is nothing to tell apart. Note that + // a lane loop counts here but not when deciding which loops separate + // one thread's copy from another's, because a warp shares its lanes' + // registers but two threads share nothing. + if (op->for_type == ForType::GPUThread || op->for_type == ForType::GPULane) { + ScopedValue bind(in_threads, true); + IRVisitor::visit(op); + } else { + IRVisitor::visit(op); + } + } + + void visit(const Realize *op) override { + // Only memory that is private to a thread, and only when the + // allocation is outside the loops over threads. An allocation with an + // automatic memory type that lands outside them goes to shared memory, + // which the threads of a block really do share. + if (!in_threads && + (op->memory_type == MemoryType::Register || + op->memory_type == MemoryType::Stack)) { + check(op); + } + IRVisitor::visit(op); + } + + void check(const Realize *op) { + FindAccesses finder(op->name); + op->body.accept(&finder); + + int thread_dims = 0; + for (const Access &a : finder.accesses) { + thread_dims = std::max(thread_dims, std::min(a.thread_depth, 3)); + internal_assert(a.args.size() == finder.accesses[0].args.size()) + << "Accesses to " << op->name << " disagree about how many " + << "dimensions it has\n"; + } + if (thread_dims == 0) { + // Only one thread runs this, so there is no one to talk to. + return; + } + + // A thread's own copy only holds what that thread put there, so + // every load has to be of something this same thread already stored. + // Comparing the arguments one dimension at a time is what makes this + // provable; the flattened index of the same access would not be. + // + // Two threads storing to one site is not itself a problem. A tail + // strategy makes neighbouring threads recompute the same values, and + // each still reads what it wrote. + // The region a thread touches is in terms of the loops it runs + // inside its own part, so those are bounded. The thread itself is left + // symbolic: bounding it would widen every region to cover all threads, + // which is what we are trying to tell apart. + Scope bounds; + for (const auto &b : finder.loop_bounds) { + bounds.push(b.first, b.second); + } + // How many threads there are is still worth knowing, to settle the + // clamp a tail strategy puts on the last thread's part. That only + // simplifies the expression; it does not go into the region. + Scope thread_bounds; + for (int i = 0; i < thread_dims; i++) { + const Expr &extent = finder.thread_extents[i]; + if (extent.defined() && is_const(extent)) { + thread_bounds.push(gpu_thread_name(i), Interval(0, simplify(extent - 1))); + } + } + + // The region each access touches, by dimension. A tail strategy wraps + // the clamp on the last thread's part in a likely intrinsic, which + // stops the simplifier folding it away once the number of threads is + // known. + vector> regions(finder.accesses.size()); + for (size_t i = 0; i < finder.accesses.size(); i++) { + for (const Expr &arg : finder.accesses[i].canonical_args) { + Expr e = simplify(remove_likelies(arg), thread_bounds); + regions[i].push_back(bounds_of_expr_in_scope(e, bounds)); + } + } + + for (size_t l = 0; l < finder.accesses.size(); l++) { + const Access &load = finder.accesses[l]; + if (load.is_store) { + continue; + } + bool ok = false; + // Stores later in the list have not happened yet. Ones earlier + // have, except across the arms of an if, which doesn't matter here: + // any value the output depends on was stored by some thread, so if + // no other thread stored this one, this thread did. A site this + // thread never wrote holds a value nothing depends on, like the + // garbage that pads out a vector. + for (size_t s = 0; s < l && !ok; s++) { + const Access &store = finder.accesses[s]; + // The store has to be in at least as many loops over threads, + // or it is the work of one thread standing in for all of them. + if (!store.is_store || store.thread_depth < load.thread_depth) { + continue; + } + ok = true; + for (size_t i = 0; i < regions[l].size() && ok; i++) { + const Interval &want = regions[l][i], &have = regions[s][i]; + ok = (want.has_lower_bound() && want.has_upper_bound() && + have.has_lower_bound() && have.has_upper_bound() && + can_prove(have.min <= want.min && want.max <= have.max)); + } + } + if (!ok) { + report(op, finder.accesses, load); + } + } + } + + void report(const Realize *op, const vector &accesses, const Access &load) { + std::ostringstream accessed; + for (const Access &a : accesses) { + accessed << " " << name_and_args(op->name, a.args) + << (a.is_store ? " (stored)" : " (loaded)") << "\n"; + } + user_error + << "The allocation " << op->name << " is scheduled to live in " + << op->memory_type << " memory, which is private to a GPU thread, but it is " + << "scheduled outside the loops over GPU threads, so every thread gets its " + << "own copy of it rather than sharing one. Halide could not prove that each " + << "thread keeps to its own part of it, so a thread may be relying on a value " + << "another thread was responsible for, which it does not have. It is " + << "loaded at:\n " << name_and_args(op->name, load.args) + << "\nwhich is not within what this thread stores. It is accessed at:\n" + << accessed.str() + << "Either schedule " << op->name << " inside the loops over GPU threads, or " + << "store it in GPUShared memory, which the threads of a block do share.\n"; + } +}; + +} // namespace + +void check_gpu_cross_talk(const Stmt &s) { + CheckCrossTalk checker; + s.accept(&checker); +} + +} // namespace Internal +} // namespace Halide diff --git a/src/CheckGPUCrossTalk.h b/src/CheckGPUCrossTalk.h new file mode 100644 index 000000000000..ea37dbd8a9ef --- /dev/null +++ b/src/CheckGPUCrossTalk.h @@ -0,0 +1,35 @@ +#ifndef HALIDE_CHECK_GPU_CROSS_TALK_H +#define HALIDE_CHECK_GPU_CROSS_TALK_H + +/** \file + * + * Defines a lowering pass that checks that allocations in memory private to a + * GPU thread are not shared between GPU threads. + */ + +#include "Expr.h" + +namespace Halide { +namespace Internal { + +/** Check that no allocation in memory private to a GPU thread, scheduled + * outside the loops over GPU threads, is accessed by more than one thread. + * Such an allocation gives every thread its own copy rather than one copy + * shared by the block, so a thread that touches a part of it another thread + * was responsible for gets its own copy of that part instead. Raises a user + * error if it cannot prove each thread keeps to its own part. + * + * Validation usually wants to be as early in lowering as possible, but this + * check is pinned between two passes. It must run before storage flattening, + * while the arguments of an access are still separated by dimension: once they + * have been flattened into a single index, showing that two threads cannot + * meet requires undoing the flattening, which needs modular arithmetic the + * simplifier will not do. It must run after storage folding, which rewrites + * access indices modulo a fold factor, and so can make two threads that were + * touching separate parts of an allocation touch the same part. */ +void check_gpu_cross_talk(const Stmt &s); + +} // namespace Internal +} // namespace Halide + +#endif diff --git a/src/Lower.cpp b/src/Lower.cpp index e179376e05c9..327382f296eb 100644 --- a/src/Lower.cpp +++ b/src/Lower.cpp @@ -20,6 +20,7 @@ #include "BoundsInference.h" #include "CSE.h" #include "CanonicalizeGPUVars.h" +#include "CheckGPUCrossTalk.h" #include "ClampUnsafeAccesses.h" #include "Debug.h" #include "DebugArguments.h" @@ -293,6 +294,12 @@ void lower_impl(const vector &output_funcs, s = bound_small_allocations(s); log("Lowering after bounding small realizations:", s); + // After storage folding, which can make two threads touch the same part of + // an allocation, and before storage flattening, which makes the check much + // harder. See CheckGPUCrossTalk.h. + debug(1) << "Checking for GPU cross-talk...\n"; + check_gpu_cross_talk(s); + debug(1) << "Performing storage flattening...\n"; s = storage_flattening(s, outputs, env, t); log("Lowering after storage flattening:", s); diff --git a/test/correctness/CMakeLists.txt b/test/correctness/CMakeLists.txt index c82d5d5ca513..5ca439110784 100644 --- a/test/correctness/CMakeLists.txt +++ b/test/correctness/CMakeLists.txt @@ -175,6 +175,7 @@ tests( gpu_object_lifetime_2.cpp gpu_object_lifetime_3.cpp gpu_param_allocation.cpp + gpu_register_at_block_level.cpp gpu_reuse_shared_memory.cpp gpu_specialize.cpp gpu_store_in_register_with_no_lanes_loop.cpp diff --git a/test/correctness/gpu_register_at_block_level.cpp b/test/correctness/gpu_register_at_block_level.cpp new file mode 100644 index 000000000000..12ed2c467039 --- /dev/null +++ b/test/correctness/gpu_register_at_block_level.cpp @@ -0,0 +1,163 @@ +#include "Halide.h" +#include + +using namespace Halide; + +// An allocation in registers outside the loops over GPU threads gives every +// thread its own copy of it, so it is only usable if each thread keeps to its +// own part. These are the ways of doing that. + +int main(int argc, char **argv) { + Target target = get_jit_target_from_environment(); + if (!target.has_gpu_feature()) { + printf("[SKIP] No GPU target enabled.\n"); + return 0; + } + + { + // Each thread computes one element and reads back the one it computed. + Func f("f"), g("g"); + Var x("x"), y("y"), xi("xi"), yi("yi"); + f(x, y) = x + y * 1000; + g(x, y) = f(x, y) * 2; + g.gpu_tile(x, y, x, y, xi, yi, 16, 16); + f.compute_at(g, x).store_in(MemoryType::Register).gpu_threads(x, y); + + Buffer result = g.realize({256, 256}, target); + for (int y = 0; y < 256; y++) { + for (int x = 0; x < 256; x++) { + int correct = (x + y * 1000) * 2; + if (result(x, y) != correct) { + printf("one element per thread: result(%d, %d) = %d instead of %d\n", + x, y, result(x, y), correct); + return 1; + } + } + } + } + + { + // Each thread computes several elements and reads back several of + // them, which is the shape a thread holding a tile of an accumulator + // has. + Func f("f"), g("g"); + Var x("x"), y("y"), xi("xi"), yi("yi"), xii("xii"); + f(x, y) = x + y * 1000; + g(x, y) = f(x, y) + f(x, y) * 2; + g.gpu_tile(x, y, x, y, xi, yi, 32, 8) + .split(xi, xi, xii, 4) + .unroll(xii); + f.compute_at(g, xi).store_in(MemoryType::Register).unroll(x).unroll(y); + + Buffer result = g.realize({256, 256}, target); + for (int y = 0; y < 256; y++) { + for (int x = 0; x < 256; x++) { + int correct = (x + y * 1000) * 3; + if (result(x, y) != correct) { + printf("several elements per thread: result(%d, %d) = %d instead of %d\n", + x, y, result(x, y), correct); + return 1; + } + } + } + } + + { + // A tile per thread, walked by serial loops rather than unrolled ones, + // with a tail strategy that lets neighbouring tiles overlap. Threads + // recompute each other's values but each still reads only its own. + Func f("f"), g("g"); + Var x("x"), y("y"), xo("xo"), yo("yo"), xi("xi"), yi("yi"), xii("xii"), yii("yii"); + Var fxo("fxo"), fyo("fyo"), fxi("fxi"), fyi("fyi"); + f(x, y) = x + y * 1000; + g(x, y) = f(x, y) * 2; + g.split(x, xo, xi, 32) + .split(y, yo, yi, 32) + .split(xi, xi, xii, 2) + .split(yi, yi, yii, 2) + .reorder(xii, yii, xi, yi, xo, yo) + .gpu_blocks(xo, yo) + .gpu_threads(xi, yi); + f.compute_at(g, xo) + .store_in(MemoryType::Register) + .split(x, fxo, fxi, 2) + .split(y, fyo, fyi, 2) + .reorder(fxi, fyi, fxo, fyo) + .gpu_threads(fxo, fyo); + + Buffer result = g.realize({128, 128}, target); + for (int y = 0; y < 128; y++) { + for (int x = 0; x < 128; x++) { + int correct = (x + y * 1000) * 2; + if (result(x, y) != correct) { + printf("tile per thread: result(%d, %d) = %d instead of %d\n", + x, y, result(x, y), correct); + return 1; + } + } + } + } + + { + // Two stages that agree about which thread owns which site. + Func f("f"), g("g"); + Var x("x"), y("y"), xi("xi"), yi("yi"); + f(x, y) = x + y; + f(x, y) += x + y; + g(x, y) = f(x, y); + g.gpu_tile(x, y, x, y, xi, yi, 16, 16); + f.compute_at(g, x).store_in(MemoryType::Register).gpu_threads(x, y); + f.update().gpu_threads(x, y); + + Buffer result = g.realize({64, 64}, target); + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + if (result(x, y) != 2 * (x + y)) { + printf("two stages: result(%d, %d) = %d instead of %d\n", + x, y, result(x, y), 2 * (x + y)); + return 1; + } + } + } + } + + { + // A tile per thread, read back through a wrapper computed inside the + // loops over threads. The wrapper's own loops start at the thread's + // part of the allocation, so their bounds speak of the thread rather + // than being constants, and they still have to bound how far it + // reaches. + Func f("f"), g("g"); + Var x("x"), y("y"), xi("xi"), yi("yi"), xii("xii"), yii("yii"); + f(x, y) = x + y * 100; + g(x, y) = f(x, y) * 2; + g.tile(x, y, xi, yi, 32, 16) + .tile(xi, yi, xii, yii, 2, 2) + .gpu_blocks(x, y) + .gpu_threads(xi, yi) + .unroll(xii) + .unroll(yii); + f.compute_at(g, x) + .store_in(MemoryType::Register) + .tile(x, y, xii, yii, 2, 2) + .gpu_threads(x, y) + .unroll(xii) + .unroll(yii); + f.in().compute_at(g, xi).unroll(x).unroll(y); + + Buffer result = g.realize({64, 64}, target); + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + int correct = (x + y * 100) * 2; + if (result(x, y) != correct) { + printf("wrapper: result(%d, %d) = %d instead of %d\n", + x, y, result(x, y), correct); + return 1; + } + } + } + } + + printf("Success!\n"); + return 0; +} diff --git a/test/error/CMakeLists.txt b/test/error/CMakeLists.txt index 377f672cb0a7..5ff9f97d944d 100644 --- a/test/error/CMakeLists.txt +++ b/test/error/CMakeLists.txt @@ -73,6 +73,11 @@ tests( func_tuple_update_types_mismatch.cpp fuse_same_var.cpp fuse_vectorized_var_with_rvar.cpp + gpu_register_crosstalk.cpp + gpu_register_shifted_between_threads.cpp + gpu_register_stages_disagree.cpp + gpu_register_stored_by_one_thread.cpp + gpu_register_stored_by_one_warp.cpp hoist_storage_extern.cpp hoist_storage_root_without_compute_root.cpp hoist_storage_without_compute_at.cpp diff --git a/test/error/gpu_register_crosstalk.cpp b/test/error/gpu_register_crosstalk.cpp new file mode 100644 index 000000000000..df6513003655 --- /dev/null +++ b/test/error/gpu_register_crosstalk.cpp @@ -0,0 +1,32 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Target target = get_jit_target_from_environment(); + if (!target.has_gpu_feature()) { + printf("[SKIP] No GPU target enabled.\n"); + // An error test has to report an error even when it skips. + _halide_user_assert(0); + } + + Func f("f"), g("g"); + Var x("x"), y("y"), xi("xi"), yi("yi"); + + f(x, y) = x + y * 1000; + // The second term reads the value the neighbouring thread computed. + g(x, y) = f(x, y) * 2 + f(x - 1, y - 1); + + g.gpu_tile(x, y, x, y, xi, yi, 16, 16); + + // f lives in registers, which are private to a thread, but it is computed + // at the block level by all the threads together, so no thread has the + // whole of it. + f.compute_at(g, x).store_in(MemoryType::Register).gpu_threads(x, y); + + g.compile_jit(target); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/gpu_register_shifted_between_threads.cpp b/test/error/gpu_register_shifted_between_threads.cpp new file mode 100644 index 000000000000..60602268a8a4 --- /dev/null +++ b/test/error/gpu_register_shifted_between_threads.cpp @@ -0,0 +1,28 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Target target = get_jit_target_from_environment(); + if (!target.has_gpu_feature()) { + printf("[SKIP] No GPU target enabled.\n"); + // An error test has to report an error even when it skips. + _halide_user_assert(0); + } + + Func g("g"), f("f"); + Var x("x"), y("y"), xi("xi"), yi("yi"); + + g(x, y) = x + y; + // One value of g per value of f, but not the one this thread stored. + f(x, y) = g(2 * x, 2 * y); + + f.gpu_tile(x, y, x, y, xi, yi, 16, 16); + g.compute_at(f, x).store_in(MemoryType::Register).gpu_threads(x, y); + + f.compile_jit(target); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/gpu_register_stages_disagree.cpp b/test/error/gpu_register_stages_disagree.cpp new file mode 100644 index 000000000000..dc6f76e3672f --- /dev/null +++ b/test/error/gpu_register_stages_disagree.cpp @@ -0,0 +1,32 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Target target = get_jit_target_from_environment(); + if (!target.has_gpu_feature()) { + printf("[SKIP] No GPU target enabled.\n"); + // An error test has to report an error even when it skips. + _halide_user_assert(0); + } + + Func f("f"), g("g"); + Var x("x"), y("y"), xi("xi"), yi("yi"); + + f(x, y) = x + y; + f(x, y) += x + y; + g(x, y) = f(x, y); + + g.gpu_tile(x, y, x, y, xi, yi, 16, 16); + f.compute_at(g, x).store_in(MemoryType::Register).gpu_threads(x, y); + + // The update maps threads to sites transposed relative to the pure + // definition, so it reads sites a different thread initialised. + f.update().reorder(y, x).gpu_threads(y, x); + + g.compile_jit(target); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/gpu_register_stored_by_one_thread.cpp b/test/error/gpu_register_stored_by_one_thread.cpp new file mode 100644 index 000000000000..f103b2a7ee9b --- /dev/null +++ b/test/error/gpu_register_stored_by_one_thread.cpp @@ -0,0 +1,32 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Target target = get_jit_target_from_environment(); + if (!target.has_gpu_feature()) { + printf("[SKIP] No GPU target enabled.\n"); + // An error test has to report an error even when it skips. + _halide_user_assert(0); + } + + Func f("f"), g("g"); + Var x("x"), y("y"), xi("xi"), yi("yi"); + + f() = 42; + g(x, y) = f() + x; + + g.gpu_tile(x, y, x, y, xi, yi, 16, 16); + + // f is computed at the block level with no loops over threads of its own, + // so fusing the thread loops leaves its store guarded by a test that only + // the first thread passes. Every other thread would read its own copy of + // an allocation only the first thread wrote. + f.compute_at(g, x).store_in(MemoryType::Register); + + g.compile_jit(target); + + printf("Success!\n"); + return 0; +} diff --git a/test/error/gpu_register_stored_by_one_warp.cpp b/test/error/gpu_register_stored_by_one_warp.cpp new file mode 100644 index 000000000000..19d44c4bcc4b --- /dev/null +++ b/test/error/gpu_register_stored_by_one_warp.cpp @@ -0,0 +1,30 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Target target = get_jit_target_from_environment(); + if (!target.has_gpu_feature()) { + printf("[SKIP] No GPU target enabled.\n"); + // An error test has to report an error even when it skips. + _halide_user_assert(0); + } + + Func g("g"), f("f"); + Var x("x"), y("y"), xi("xi"), yi("yi"); + + g(x) = x; + f(x, y) = g(x); + + f.gpu_tile(x, y, x, y, xi, yi, 16, 16); + + // g has no y, so only the threads at y == 0 store it, but every y reads + // it. Each of the others would read its own copy, which it never wrote. + g.compute_at(f, x).store_in(MemoryType::Register).gpu_threads(x); + + f.compile_jit(target); + + printf("Success!\n"); + return 0; +}