Skip to content

Commit cfeb793

Browse files
abadamsclaudehalide-ci[bot]
authored
Shrink-wrap block-level GPU register allocations (#9343)
* Give each site of a GPU register allocation its own registers An allocation in MemoryType::Register outside the loops over GPU threads is storage private to a thread, so what looks like one allocation of many elements is really a handful of registers held by each thread. The cross-talk check established that each thread keeps to its own part; this shrinks the allocation to just that part. Two accesses by one thread are to the same elements when the distance between them is the same whatever thread it is, because the thread cancels when only comparing accesses made by the same one. That is the question get_subtile already answers for tile memory, so ask it: group the accesses into sets that are each identical or disjoint, reject a partial overlap, and give each set registers of its own. Nothing about how an access covers its elements matters, because the registers a set gets are its own, so a dense ramp reaches them all. A thread that indexes its own storage dynamically has no fixed register to use and gets a user error saying so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep the SGEMM accumulator in registers, and stage asynchronously The accumulator now lives at block level in registers, which puts the loop over the reduction above the loop over threads, so one staged panel of each input serves every thread in the block. The panels are copied from global to shared asynchronously, laid over the same grid of threads as the compute so that no thread sits idle in either phase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Apply pre-commit auto-fixes * Build the cuda mat mul app for the capability it needs Staging the inputs with asynchronous copies needs compute capability 8.0, which the app's Makefile asks for but its CMake build did not, so the generator refused to compile it. Raise the guard in the runner to match, so that the test skips on an older GPU rather than failing on one. Also report an error when the dynamic index test skips, since an error test that returns cleanly reads as a failure to the Makefile's harness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Use the lambda visitors to find and rewrite the accesses Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: halide-ci[bot] <266445882+halide-ci[bot]@users.noreply.github.com>
1 parent 3873d7c commit cfeb793

12 files changed

Lines changed: 377 additions & 46 deletions

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ SOURCE_FILES = \
562562
Prefetch.cpp \
563563
PrintLoopNest.cpp \
564564
Profiling.cpp \
565+
PromoteGPURegisters.cpp \
565566
PurifyIndexMath.cpp \
566567
PythonExtensionGen.cpp \
567568
Qualify.cpp \
@@ -770,6 +771,7 @@ HEADER_FILES = \
770771
Prefetch.h \
771772
PrefetchDirective.h \
772773
Profiling.h \
774+
PromoteGPURegisters.h \
773775
PurifyIndexMath.h \
774776
PythonExtensionGen.h \
775777
Qualify.h \

apps/cuda_mat_mul/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ find_package(Halide REQUIRED)
2828
add_halide_generator(mat_mul.generator SOURCES mat_mul_generator.cpp)
2929

3030
# Filters
31-
add_halide_library(mat_mul FROM mat_mul.generator FEATURES cuda cuda_capability_50 PARAMS size=1024)
31+
add_halide_library(mat_mul FROM mat_mul.generator FEATURES cuda cuda_capability_80 PARAMS size=1024)
3232

3333
# Main executable
3434
add_executable(runner runner.cpp)

apps/cuda_mat_mul/Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ include ../support/Makefile.inc
22

33
MATRIX_SIZE ?= 1024
44

5-
CUDA_SDK ?= /usr/local/cuda-10.0
5+
CUDA_TARGET ?= host-cuda-cuda_capability_80
6+
7+
CUDA_SDK ?= /usr/local/cuda
68

79
CXXFLAGS += -I $(CUDA_SDK)/include
810
LDFLAGS += -L $(CUDA_SDK)/lib64 -Wl,-rpath,$(CUDA_SDK)/lib64
@@ -15,7 +17,7 @@ $(GENERATOR_BIN)/mat_mul.generator: mat_mul_generator.cpp $(GENERATOR_DEPS)
1517

1618
$(BIN)/%/mat_mul.a: $(GENERATOR_BIN)/mat_mul.generator
1719
@mkdir -p $(@D)
18-
$^ -g mat_mul -e $(GENERATOR_OUTPUTS) -o $(@D) target=host-cuda-cuda_capability_50 size=$(MATRIX_SIZE)
20+
$^ -g mat_mul -e $(GENERATOR_OUTPUTS) -o $(@D) target=$(CUDA_TARGET) size=$(MATRIX_SIZE)
1921

2022
$(BIN)/%/runner: runner.cpp $(BIN)/%/mat_mul.a
2123
@mkdir -p $(@D)

apps/cuda_mat_mul/mat_mul_generator.cpp

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,21 @@ void set_alignment_and_bounds(OutputImageParam p, int size) {
1515
class MatMul : public Halide::Generator<MatMul> {
1616
public:
1717
GeneratorParam<int> size{"size", 1024};
18+
// The tile of the output one block computes, the piece of it one thread
19+
// holds in registers, and how much of the reduction is staged at a time.
20+
GeneratorParam<int> block_x{"block_x", 64};
21+
GeneratorParam<int> block_y{"block_y", 64};
22+
GeneratorParam<int> reg_x{"reg_x", 4};
23+
GeneratorParam<int> reg_y{"reg_y", 8};
24+
GeneratorParam<int> chunk{"chunk", 32};
1825
Input<Buffer<float, 2>> A{"A"};
1926
Input<Buffer<float, 2>> B{"B"};
2027

2128
Output<Buffer<float, 2>> out{"out"};
2229

2330
void generate() {
24-
// 688 us on an RTX 2060
25-
// cublas is 512 us on the same card
31+
// 162 us on an RTX 5060 Ti
32+
// cublas is 150 us on the same card
2633

2734
Var x("x"), y("y"), p("p");
2835

@@ -35,24 +42,64 @@ class MatMul : public Halide::Generator<MatMul> {
3542
RVar rxo, rxi;
3643

3744
if (!using_autoscheduler()) {
45+
const int bx = block_x, by = block_y, rx = reg_x, ry = reg_y, k = chunk;
46+
const int tx = bx / rx, ty = by / ry;
47+
48+
// A block computes a block_x by block_y tile of the output with
49+
// tx by ty threads, each holding a reg_x by reg_y tile of the
50+
// accumulator in registers. The accumulator lives at block level
51+
// so that the loop over the reduction can sit above the loop over
52+
// threads, which lets one staged panel of each input serve every
53+
// thread in the block.
3854
out.bound(x, 0, size)
3955
.bound(y, 0, size)
40-
.tile(x, y, xi, yi, 64, 16)
41-
.tile(xi, yi, xii, yii, 4, 8)
56+
.tile(x, y, xi, yi, bx, by)
57+
.tile(xi, yi, xii, yii, rx, ry)
4258
.gpu_blocks(x, y)
4359
.gpu_threads(xi, yi)
60+
.vectorize(xii)
61+
.unroll(yii);
62+
63+
prod.compute_at(out, x)
64+
.store_in(MemoryType::Register)
65+
.tile(x, y, xii, yii, rx, ry)
66+
.gpu_threads(x, y)
4467
.unroll(xii)
4568
.unroll(yii);
46-
prod.compute_at(out, xi)
47-
.vectorize(x)
48-
.unroll(y)
49-
.update()
50-
.reorder(x, y, r)
51-
.vectorize(x)
52-
.unroll(y)
53-
.unroll(r, 8);
54-
A.in().compute_at(prod, r).vectorize(_0).unroll(_1);
55-
B.in().compute_at(prod, r).vectorize(_0).unroll(_1);
69+
70+
prod.update()
71+
.split(r, rxo, rxi, k)
72+
.tile(x, y, xii, yii, rx, ry)
73+
.reorder(xii, yii, rxi, x, y, rxo)
74+
.gpu_threads(x, y)
75+
.unroll(xii)
76+
.unroll(yii)
77+
.unroll(rxi);
78+
79+
prod.in().compute_at(out, xi).unroll(x).unroll(y);
80+
81+
// One panel of each input per block per step of the reduction,
82+
// copied from global to shared by all the threads together. Each
83+
// thread moves four floats at a time, which is the widest
84+
// asynchronous copy the hardware has. Both panels are laid over
85+
// the same grid of threads as the compute, so that no thread sits
86+
// idle in either phase.
87+
Var v("v"), t("t"), ti("ti"), tj("tj"), to("to");
88+
auto stage = [&](Func f) {
89+
f.compute_at(prod, rxo)
90+
.store_in(MemoryType::GPUSharedAsync)
91+
.split(_0, _0, v, 4)
92+
.fuse(_0, _1, t)
93+
.split(t, t, ti, tx)
94+
.split(t, to, tj, ty)
95+
.gpu_threads(ti, tj)
96+
.reorder(to, ti, tj)
97+
.unroll(to)
98+
.vectorize(v);
99+
};
100+
stage(A.in());
101+
stage(B.in());
102+
A.in().compute_with(B.in(), ti);
56103

57104
set_alignment_and_bounds(A, size);
58105
set_alignment_and_bounds(B, size);

apps/cuda_mat_mul/runner.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ using Halide::Runtime::Buffer;
1010
using Halide::Tools::benchmark;
1111

1212
int main(int argc, char **argv) {
13-
// Our Generator is compiled using cuda_capability_50; if the system running this
14-
// test doesn't have at least that, quietly skip the test.
13+
// Our Generator is compiled using cuda_capability_80, because it stages its
14+
// inputs with asynchronous copies; if the system running this test doesn't
15+
// have at least that, quietly skip the test.
1516
const auto *interface = halide_cuda_device_interface();
1617
assert(interface->compute_capability != nullptr);
1718
int major, minor;
1819
int err = interface->compute_capability(nullptr, &major, &minor);
1920
assert(err == 0);
2021
int ver = major * 10 + minor;
21-
if (ver < 50) {
22-
printf("[SKIP] This system supports only Cuda compute capability %d.%d, but compute capability 5.0+ is required.\n", major, minor);
22+
if (ver < 80) {
23+
printf("[SKIP] This system supports only Cuda compute capability %d.%d, but compute capability 8.0+ is required.\n", major, minor);
2324
return 0;
2425
}
2526

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ target_sources(
177177
Prefetch.h
178178
PrefetchDirective.h
179179
Profiling.h
180+
PromoteGPURegisters.h
180181
PurifyIndexMath.h
181182
PythonExtensionGen.h
182183
Qualify.h
@@ -356,6 +357,7 @@ target_sources(
356357
Prefetch.cpp
357358
PrintLoopNest.cpp
358359
Profiling.cpp
360+
PromoteGPURegisters.cpp
359361
PurifyIndexMath.cpp
360362
PythonExtensionGen.cpp
361363
Qualify.cpp

src/Lower.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#include "PartitionLoops.h"
5353
#include "Prefetch.h"
5454
#include "Profiling.h"
55+
#include "PromoteGPURegisters.h"
5556
#include "PurifyIndexMath.h"
5657
#include "Qualify.h"
5758
#include "RealizationOrder.h"
@@ -364,6 +365,10 @@ void lower_impl(const vector<Function> &output_funcs,
364365
t.has_feature(Target::Vulkan)) {
365366
debug(1) << "Injecting per-block gpu synchronization...\n";
366367
s = fuse_gpu_thread_loops(s);
368+
log("Lowering after fusing GPU thread loops:", s);
369+
370+
debug(1) << "Promoting GPU register allocations...\n";
371+
s = promote_gpu_registers(s);
367372
log("Lowering after injecting per-block gpu synchronization:", s);
368373
}
369374

src/PromoteGPURegisters.cpp

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#include "PromoteGPURegisters.h"
2+
3+
#include "IR.h"
4+
#include "IREquality.h"
5+
#include "IRMutator.h"
6+
#include "IROperator.h"
7+
#include "IRVisitor.h"
8+
#include "MultiRamp.h"
9+
10+
#include <map>
11+
12+
namespace Halide {
13+
namespace Internal {
14+
15+
using std::map;
16+
using std::string;
17+
using std::vector;
18+
19+
namespace {
20+
21+
// Every access to the allocation, in the order they appear.
22+
vector<Expr> find_accesses(const Stmt &s, const string &alloc) {
23+
vector<Expr> indices;
24+
auto note = [&](auto *self, const auto *op) {
25+
if (op->name == alloc) {
26+
indices.push_back(op->index);
27+
}
28+
self->visit_base(op);
29+
};
30+
visit_with(
31+
s, [&](auto *self, const Store *op) { note(self, op); },
32+
[&](auto *self, const Load *op) { note(self, op); });
33+
return indices;
34+
}
35+
36+
// Which kinds of loop over the threads of a block appear in some IR.
37+
struct LoopKinds {
38+
bool threads = false, lanes = false;
39+
};
40+
41+
LoopKinds loop_kinds(const Stmt &s) {
42+
LoopKinds kinds;
43+
visit_with(s, [&](auto *self, const For *op) {
44+
kinds.threads = kinds.threads || op->for_type == ForType::GPUThread;
45+
kinds.lanes = kinds.lanes || op->for_type == ForType::GPULane;
46+
self->visit_base(op);
47+
});
48+
return kinds;
49+
}
50+
51+
// Replace each access with the one worked out for it below.
52+
Stmt rewrite_accesses(const Stmt &s, const string &alloc,
53+
const map<Expr, Expr, IRDeepCompare> &rewritten) {
54+
auto index_for = [&](const Expr &index) {
55+
auto it = rewritten.find(index);
56+
internal_assert(it != rewritten.end());
57+
return it->second;
58+
};
59+
return mutate_with(
60+
s,
61+
[&](auto *self, const Store *op) {
62+
Stmt s = self->visit_base(op);
63+
if (op->name == alloc) {
64+
const Store *store = s.as<Store>();
65+
s = store->with(store->value, index_for(store->index), store->predicate,
66+
ModulusRemainder());
67+
}
68+
return s;
69+
},
70+
[&](auto *self, const Load *op) {
71+
Expr e = self->visit_base(op);
72+
if (op->name == alloc) {
73+
const Load *load = e.as<Load>();
74+
e = load->with(index_for(load->index), load->predicate, ModulusRemainder());
75+
}
76+
return e;
77+
});
78+
}
79+
80+
class PromoteGPURegisters : public IRMutator {
81+
protected:
82+
using IRMutator::visit;
83+
84+
bool in_threads = false;
85+
vector<const Allocate *> pending;
86+
87+
Stmt visit(const Allocate *op) override {
88+
LoopKinds kinds = loop_kinds(op->body);
89+
// An allocation with a loop over lanes inside it is warp-level
90+
// storage, which LowerWarpShuffles stripes across the lanes. Leave it
91+
// alone. Without a loop over threads there is nowhere to put this one,
92+
// and whoever runs it already has it to themselves.
93+
if (!in_threads && op->memory_type == MemoryType::Register &&
94+
kinds.threads && !kinds.lanes) {
95+
// Pick it up, and put it back inside the loops over threads.
96+
pending.push_back(op);
97+
return mutate(op->body);
98+
}
99+
return IRMutator::visit(op);
100+
}
101+
102+
Stmt visit(const For *op) override {
103+
if (op->for_type != ForType::GPUThread || pending.empty()) {
104+
ScopedValue<bool> bind(in_threads,
105+
in_threads || op->for_type == ForType::GPUThread ||
106+
op->for_type == ForType::GPULane);
107+
return IRMutator::visit(op);
108+
}
109+
110+
// The outermost loop over threads with allocations to place. Everything
111+
// private to a thread goes inside it.
112+
vector<const Allocate *> allocs;
113+
allocs.swap(pending);
114+
115+
Stmt body = op->body;
116+
for (const Allocate *alloc : allocs) {
117+
body = promote(alloc, body);
118+
}
119+
{
120+
ScopedValue<bool> bind(in_threads, true);
121+
body = mutate(body);
122+
}
123+
return op->with(op->min, op->max, body);
124+
}
125+
126+
// Give each site its own registers, and wrap the body in the smaller
127+
// allocation.
128+
Stmt promote(const Allocate *op, Stmt body) {
129+
vector<Expr> accesses = find_accesses(body, op->name);
130+
131+
// Each access covers a set of elements, and get_subtile partitions the
132+
// accesses between the distinct sets. Nothing about the layout of a set
133+
// matters here, because the registers it gets are its own, so a dense
134+
// ramp reaches all of them.
135+
vector<MultiRamp> subtiles;
136+
map<Expr, Expr, IRDeepCompare> rewritten;
137+
string description = "the allocation " + op->name +
138+
", which is scheduled to live in Register memory outside the "
139+
"loops over GPU threads";
140+
for (const Expr &index : accesses) {
141+
int subtile = get_subtile(index, description, &subtiles);
142+
// Every subtile has the same shape, and so the same number of
143+
// lanes, because get_subtile rejects accesses that don't.
144+
int lanes = subtiles[subtile].total_lanes();
145+
Expr base = make_const(index.type().element_of(), subtile * lanes);
146+
rewritten[index] =
147+
lanes == 1 ? base : Ramp::make(base, make_one(base.type()), lanes);
148+
}
149+
150+
int size = subtiles.empty() ? 0 : (int)subtiles.size() * subtiles[0].total_lanes();
151+
body = rewrite_accesses(body, op->name, rewritten);
152+
153+
return op->with({make_const(Int(32), size)}, op->condition, body);
154+
}
155+
};
156+
157+
} // namespace
158+
159+
Stmt promote_gpu_registers(const Stmt &s) {
160+
return PromoteGPURegisters()(s);
161+
}
162+
163+
} // namespace Internal
164+
} // namespace Halide

0 commit comments

Comments
 (0)