Skip to content

Commit ee6cef8

Browse files
committed
Add event to GEMM batches in POTRF
Signed-off-by: Joseph Schuchart <joseph.schuchart@stonybrook.edu>
1 parent 4e992b7 commit ee6cef8

1 file changed

Lines changed: 75 additions & 14 deletions

File tree

examples/potrf/potrf.h

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,13 @@ namespace potrf {
9393
* Small round-robin pool of pinned host / device pointer-array triples used
9494
* to build a single cublasDgemmBatched/hipblasDgemmBatched call from the
9595
* ttg::device::batch collected via ttg::device::coop (see make_gemm_batched
96-
* below). `num_slots` is sized generously so that an in-flight batch -- in
97-
* practice bounded by the number of concurrent device streams -- does not
98-
* reuse a slot before its batched kernel has completed. This is
99-
* illustrative example code, not a general-purpose device allocator.
96+
* below). A larger `num_slots` reduces contention (how often acquire()
97+
* blocks, see below), but correctness does not depend on sizing it to
98+
* exceed the number of concurrent in-flight batches: acquire()/release()
99+
* use a per-slot event to block a new leader until the previous occupant's
100+
* batched kernel/copies have actually completed, so a slot is never reused
101+
* while still in flight. This is illustrative example code, not a
102+
* general-purpose device allocator.
100103
* Works (as a degenerate size-1 batch) even if the linked PaRSEC lacks
101104
* kernel-batching support; no TTG_HAVE_PARSEC_DEV_BATCH guard is needed.
102105
*/
@@ -115,13 +118,15 @@ namespace potrf {
115118
cudaMalloc(&s.devA, max_batch_size * sizeof(const T *));
116119
cudaMalloc(&s.devB, max_batch_size * sizeof(const T *));
117120
cudaMalloc(&s.devC, max_batch_size * sizeof(T *));
121+
cudaEventCreateWithFlags(&s.ready, cudaEventDisableTiming);
118122
#elif defined(TTG_ENABLE_HIP)
119123
hipHostRegister(s.hostA.data(), max_batch_size * sizeof(const T *), hipHostRegisterDefault);
120124
hipHostRegister(s.hostB.data(), max_batch_size * sizeof(const T *), hipHostRegisterDefault);
121125
hipHostRegister(s.hostC.data(), max_batch_size * sizeof(T *), hipHostRegisterDefault);
122126
hipMalloc(&s.devA, max_batch_size * sizeof(const T *));
123127
hipMalloc(&s.devB, max_batch_size * sizeof(const T *));
124128
hipMalloc(&s.devC, max_batch_size * sizeof(T *));
129+
hipEventCreateWithFlags(&s.ready, hipEventDisableTiming);
125130
#endif
126131
}
127132
}
@@ -135,13 +140,15 @@ namespace potrf {
135140
cudaFree(s.devA);
136141
cudaFree(s.devB);
137142
cudaFree(s.devC);
143+
cudaEventDestroy(s.ready);
138144
#elif defined(TTG_ENABLE_HIP)
139145
hipHostUnregister(s.hostA.data());
140146
hipHostUnregister(s.hostB.data());
141147
hipHostUnregister(s.hostC.data());
142148
hipFree(s.devA);
143149
hipFree(s.devB);
144150
hipFree(s.devC);
151+
hipEventDestroy(s.ready);
145152
#endif
146153
}
147154
}
@@ -152,15 +159,55 @@ namespace potrf {
152159
const T **devA = nullptr;
153160
const T **devB = nullptr;
154161
T **devC = nullptr;
162+
#if defined(TTG_ENABLE_CUDA)
163+
cudaEvent_t ready = nullptr;
164+
#elif defined(TTG_ENABLE_HIP)
165+
hipEvent_t ready = nullptr;
166+
#endif
167+
// whether `ready` was ever recorded (i.e. this slot has a prior
168+
// occupant to wait for); false only before this slot's first release().
169+
bool ready_pending = false;
155170
};
156171

157-
/* acquire the next slot (round-robin); the caller fills hostA/hostB/hostC
158-
* (up to `count` entries) with this batch's device pointers and then
159-
* copies them to devA/devB/devC (e.g. via cudaMemcpyAsync/hipMemcpyAsync
160-
* on the current stream) before launching the batched kernel. */
172+
/* acquire the next slot (round-robin). Blocks (host-side) until the
173+
* previous occupant's batched kernel/copies -- which read this exact
174+
* slot's host/device pointer buffers -- have completed, so the caller
175+
* can safely overwrite hostA/hostB/hostC without racing an in-flight
176+
* H2D copy of the old contents. This host-side wait, not just device-
177+
* stream ordering, is required because the race is the CPU overwriting
178+
* pinned host memory while the GPU DMA engine may still be reading it;
179+
* ordering only the *device*-side work (e.g. cudaStreamWaitEvent) would
180+
* not protect the CPU write. The caller fills hostA/hostB/hostC (up to
181+
* `count` entries) with this batch's device pointers, copies them to
182+
* devA/devB/devC (e.g. via cudaMemcpyAsync/hipMemcpyAsync on the current
183+
* stream), launches the batched kernel, and must call release() with
184+
* that same stream so the next acquire() of this slot knows what to
185+
* wait for. */
161186
slot_t &acquire() {
162187
std::size_t idx = next.fetch_add(1, std::memory_order_relaxed) % slots.size();
163-
return slots[idx];
188+
auto &slot = slots[idx];
189+
if (slot.ready_pending) {
190+
#if defined(TTG_ENABLE_CUDA)
191+
cudaEventSynchronize(slot.ready);
192+
#elif defined(TTG_ENABLE_HIP)
193+
hipEventSynchronize(slot.ready);
194+
#endif
195+
}
196+
return slot;
197+
}
198+
199+
/* mark `slot` as in-use until every operation enqueued on `stream` so far
200+
* (in particular, the H2D copies and the batched kernel reading it) has
201+
* completed; must be called by the leader right after those are
202+
* enqueued, before the slot can be safely acquire()'d again. */
203+
template <typename Stream>
204+
void release(slot_t &slot, Stream stream) {
205+
#if defined(TTG_ENABLE_CUDA)
206+
cudaEventRecord(slot.ready, stream);
207+
#elif defined(TTG_ENABLE_HIP)
208+
hipEventRecord(slot.ready, stream);
209+
#endif
210+
slot.ready_pending = true;
164211
}
165212

166213
std::size_t max_batch_size;
@@ -777,6 +824,7 @@ namespace potrf {
777824
cublasDgemmBatched(cublas_handle(), CUBLAS_OP_N, CUBLAS_OP_T, mb, nnb, kb, &alpha,
778825
slot.devA, tile_mk.lda(), slot.devB, tile_nk.lda(), &beta,
779826
slot.devC, tile_mn.lda(), (int)nb);
827+
ptr_pool->release(slot, ttg::device::current_stream());
780828
#elif defined(TTG_ENABLE_HIP)
781829
hipMemcpyAsync(slot.devA, slot.hostA.data(), nb * sizeof(const T *), hipMemcpyHostToDevice,
782830
ttg::device::current_stream());
@@ -787,6 +835,7 @@ namespace potrf {
787835
hipblasDgemmBatched(hipblas_handle(), HIPBLAS_OP_N, HIPBLAS_OP_T, mb, nnb, kb, &alpha,
788836
slot.devA, tile_mk.lda(), slot.devB, tile_nk.lda(), &beta,
789837
slot.devC, tile_mn.lda(), (int)nb);
838+
ptr_pool->release(slot, ttg::device::current_stream());
790839
#endif
791840
}
792841
// followers: the leader's batched call already covers our output tile.
@@ -810,12 +859,24 @@ namespace potrf {
810859
ttg::edges(output_trsm, output_gemm), "GEMM", {"input_mk", "input_kn", "input_mn/dispatcher"},
811860
{"output_trsm", "outout_gemm"});
812861
// cublasDgemmBatched/hipblasDgemmBatched require identical (m,n,k) and
813-
// leading dimensions across the whole batch; tiles sharing the same K
814-
// column in this tiling always have matching geometry, so that is a
815-
// sufficient compatibility test. Unconditionally safe to call: it is a
816-
// no-op unless the linked PaRSEC actually supports kernel batching.
862+
// leading dimensions across the whole batch. Sharing the same K only
863+
// guarantees a common k (tile_nk.cols(), fixed by the K-th column tile);
864+
// M and N range independently over all tile indices > K, and the last
865+
// row tile of the matrix is smaller than the nominal tile size whenever
866+
// A.rows_in_matrix() is not an exact multiple of A.rows_in_tile(), so two
867+
// same-K candidates can still disagree on tile_mk.rows()/tile_nk.rows().
868+
// Compare the actual (boundary-aware) tile row-counts for M and N too --
869+
// a constant-time index computation, no data access needed.
870+
auto tile_rows_at = [rows = A.rows(), tile_rows = A.rows_in_tile(), m = A.rows_in_matrix()](std::size_t idx) {
871+
return (idx < rows - 1) ? tile_rows : m - idx * tile_rows;
872+
};
873+
// Unconditionally safe to call: it is a no-op unless the linked PaRSEC
874+
// actually supports kernel batching.
817875
tt_gemm->set_batch_matcher(
818-
[](const Key3& head, const Key3& cand) { return head[2] == cand[2]; },
876+
[tile_rows_at](const Key3& head, const Key3& cand) {
877+
return head[2] == cand[2] && tile_rows_at(head[0]) == tile_rows_at(cand[0]) &&
878+
tile_rows_at(head[1]) == tile_rows_at(cand[1]);
879+
},
819880
max_batch_size);
820881
return tt_gemm;
821882
}

0 commit comments

Comments
 (0)