Skip to content

Commit 449ade1

Browse files
q10meta-codesync[bot]
authored andcommitted
Add grid-stride loop and ROCm cap to batched_unary_embeddings_{forward,backward}_kernel (#5933)
Summary: Pull Request resolved: #5933 Tier-2 fix for HIP grid-overflow in `sparse_ops/sparse_batched_unary_embeddings.cu`. Both forward and backward kernels previously used `blockIdx.x * blockDim.x + threadIdx.x` directly with early-returns. Capping the host-side x-dim grid without first adding a grid-stride loop would silently drop work. Changes: - `batched_unary_embeddings_forward_kernel`: convert to a grid-stride loop over `b = blockIdx.x * blockDim.x + threadIdx.x; b < B; b += gridDim.x * blockDim.x` (Pattern A on the saturating x-dim). Hoisted `t = blockIdx.y`, `n = blockIdx.z`, `sum_E = table_offsets[T]`, and `table_offset = table_offsets[t]` outside the loop since y/z dims are bounded by T/N <= 65535 and don't need grid-striding. - `batched_unary_embeddings_backward_kernel`: convert to a grid-stride loop over `run_id`. Hoisted `n = blockIdx.y` and the single-element device read of `sorted_linear_indices_num_runs[0]` outside the loop. The `min(num_runs_size_0, num_runs_actual)` becomes the loop bound. The `SL == 0` early-return is replaced with `continue`. - Apply standard `#ifdef USE_ROCM min(blocks_x_uncapped, get_max_thread_blocks(stream)) #else blocks_x_uncapped #endif` cap to the x-dim of the launch grid for both kernels. y/z dims are bounded by T/N/N <= 65535 (enforced by host-side `TORCH_CHECK`s) and need no cap. Stacked on top of D105028336 (Tier-2 Diff 4/7). Plan: `/home/bensonma415/.llms/plans/sparse_ops_rocm_grid_overflow_tier2_fix.plan.md` (Diff 5/7). Reviewed By: henrylhtsang Differential Revision: D105029028 fbshipit-source-id: 5b8af5cd8395334be9bff66ea077aaec6568eefa
1 parent defd174 commit 449ade1

2 files changed

Lines changed: 179 additions & 54 deletions

File tree

fbgemm_gpu/src/sparse_ops/sparse_batched_unary_embeddings.cu

Lines changed: 73 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,25 @@ __launch_bounds__(kMaxThreads) void batched_unary_embeddings_forward_kernel(
2626
const index_t* __restrict__ indices,
2727
scalar_t* __restrict__ output // N * B * T
2828
) {
29-
index_t sum_E = table_offsets[T];
30-
auto b = blockIdx.x * blockDim.x + threadIdx.x;
31-
if (b >= B) {
32-
return;
33-
}
34-
auto t = blockIdx.y;
35-
auto n = blockIdx.z;
36-
index_t table_offset = table_offsets[t];
37-
index_t indices_start = offsets[t * B + b];
38-
index_t indices_end = offsets[t * B + b + 1];
39-
int32_t L = indices_end - indices_start;
40-
at::acc_type<scalar_t, true> sum = 0.0;
41-
for (int32_t l = 0; l < L; ++l) {
42-
auto idx = LDG(&indices[indices_start + l]);
43-
sum += weight[n * sum_E + table_offset + idx + 0];
29+
const index_t sum_E = table_offsets[T];
30+
const auto t = blockIdx.y;
31+
const auto n = blockIdx.z;
32+
const index_t table_offset = table_offsets[t];
33+
// Grid-stride over the b dimension so a capped grid (used on ROCm to avoid
34+
// the 2^32 launch-side limit) still covers all B elements. blockIdx.y/z are
35+
// already bounded by T/N <= 65535.
36+
for (auto b = blockIdx.x * blockDim.x + threadIdx.x; b < B;
37+
b += gridDim.x * blockDim.x) {
38+
index_t indices_start = offsets[t * B + b];
39+
index_t indices_end = offsets[t * B + b + 1];
40+
int32_t L = indices_end - indices_start;
41+
at::acc_type<scalar_t, true> sum = 0.0;
42+
for (int32_t l = 0; l < L; ++l) {
43+
auto idx = LDG(&indices[indices_start + l]);
44+
sum += weight[n * sum_E + table_offset + idx + 0];
45+
}
46+
output[(n * B + b) * T + t] = sum;
4447
}
45-
output[(n * B + b) * T + t] = sum;
4648
}
4749

4850
Tensor batched_unary_embeddings_forward_cuda(
@@ -66,7 +68,14 @@ Tensor batched_unary_embeddings_forward_cuda(
6668
TORCH_CHECK(T <= 65535);
6769
TORCH_CHECK(N <= 65535);
6870
int32_t threads = std::min<int32_t>(B, 512);
69-
dim3 blocks(cuda_calc_xblock_count(B, threads), T, N);
71+
// HIP enforces a hard limit of 2^32 total threads per launch (unlike CUDA,
72+
// which silently wraps). The forward kernel grid-strides over b, so capping
73+
// is correctness-preserving. y/z dims are bounded by T/N <= 65535 and need
74+
// no cap.
75+
// See: https://github.com/ROCm/hip/issues/2253
76+
const auto blocks_x = utils::cuda::cap_grid_dim_x_from_workload(
77+
B, threads, at::cuda::getCurrentCUDAStream());
78+
dim3 blocks(blocks_x, T, N);
7079
auto output = at::empty({N, B, T}, weight.options());
7180
AT_DISPATCH_INDEX_TYPES(
7281
indices.scalar_type(), "batched_unary_embeddings_forward_kernel", [&] {
@@ -127,45 +136,49 @@ __launch_bounds__(kMaxThreads) void batched_unary_embeddings_backward_kernel(
127136
const int32_t* __restrict__ sorted_linear_indices_num_runs,
128137
const int32_t info_B_num_bits,
129138
const uint32_t info_B_mask) {
130-
auto run_id = blockIdx.x * blockDim.x + threadIdx.x;
131-
auto n = blockIdx.y;
139+
const auto n = blockIdx.y;
132140
if (n >= N) {
133141
return;
134142
}
135-
if (run_id >= sorted_linear_indices_run.size(0)) {
136-
return;
137-
}
138-
if (run_id >= sorted_linear_indices_num_runs[0]) {
139-
return;
140-
}
141-
int64_t linear_index = sorted_linear_indices_run[run_id];
142-
int32_t segment_start = sorted_linear_indices_cumulative_run_lengths[run_id];
143-
int32_t segment_end =
144-
sorted_linear_indices_cumulative_run_lengths[run_id + 1];
145-
int32_t SL = segment_end - segment_start;
143+
// Hoist single-element device tensor read out of the stride loop.
144+
const auto num_runs_actual = sorted_linear_indices_num_runs[0];
145+
const auto num_runs_size_0 = sorted_linear_indices_run.size(0);
146+
const auto num_runs = ::min(num_runs_size_0, num_runs_actual);
147+
// Grid-stride over run_id so a capped grid (used on ROCm to avoid the
148+
// 2^32 launch-side limit) still covers all runs. blockIdx.y is bounded
149+
// by N <= 65535 (enforced by the host).
150+
for (auto run_id = blockIdx.x * blockDim.x + threadIdx.x; run_id < num_runs;
151+
run_id += gridDim.x * blockDim.x) {
152+
int64_t linear_index = sorted_linear_indices_run[run_id];
153+
int32_t segment_start =
154+
sorted_linear_indices_cumulative_run_lengths[run_id];
155+
int32_t segment_end =
156+
sorted_linear_indices_cumulative_run_lengths[run_id + 1];
157+
int32_t SL = segment_end - segment_start;
146158

147-
if (SL == 0) {
148-
return;
149-
}
159+
if (SL == 0) {
160+
continue;
161+
}
150162

151-
// now, each segment corresponds to exactly one table `t` and row in
152-
// that table (`idx`). Thus, we can hoist out some of the book-keeping.
153-
const auto info =
154-
reinterpret_cast<const uint32_t*>(sorted_infos)[segment_start];
155-
int t = info >> info_B_num_bits;
156-
157-
at::acc_type<scalar_t, true> grad_sum = 0.0;
158-
for (int32_t sl = 0; sl < SL; ++sl) {
159-
const auto b =
160-
reinterpret_cast<const uint32_t*>(sorted_infos)[segment_start + sl] &
161-
info_B_mask;
162-
grad_sum += grad_output[(n * B + b) * T + t];
163-
}
163+
// now, each segment corresponds to exactly one table `t` and row in
164+
// that table (`idx`). Thus, we can hoist out some of the book-keeping.
165+
const auto info =
166+
reinterpret_cast<const uint32_t*>(sorted_infos)[segment_start];
167+
int t = info >> info_B_num_bits;
164168

165-
index_t table_offset = table_offsets[t];
166-
index_t sum_E = table_offsets[T];
167-
int64_t idx = linear_index - table_offset;
168-
grad_weight[n * sum_E + table_offset + idx] = grad_sum;
169+
at::acc_type<scalar_t, true> grad_sum = 0.0;
170+
for (int32_t sl = 0; sl < SL; ++sl) {
171+
const auto b =
172+
reinterpret_cast<const uint32_t*>(sorted_infos)[segment_start + sl] &
173+
info_B_mask;
174+
grad_sum += grad_output[(n * B + b) * T + t];
175+
}
176+
177+
index_t table_offset = table_offsets[t];
178+
index_t sum_E = table_offsets[T];
179+
int64_t idx = linear_index - table_offset;
180+
grad_weight[n * sum_E + table_offset + idx] = grad_sum;
181+
}
169182
}
170183

171184
DLL_PUBLIC Tensor batched_unary_embeddings_backward_cuda(
@@ -219,8 +232,16 @@ DLL_PUBLIC Tensor batched_unary_embeddings_backward_cuda(
219232
info_B_mask);
220233

221234
int threads = std::min<int32_t>(sorted_linear_indices_run.numel(), 512);
222-
dim3 blocks(
223-
cuda_calc_xblock_count(sorted_linear_indices_run.numel(), threads), N);
235+
// HIP enforces a hard limit of 2^32 total threads per launch (unlike CUDA,
236+
// which silently wraps). The backward kernel grid-strides over run_id, so
237+
// capping is correctness-preserving. y dim is bounded by N <= 65535 and
238+
// needs no cap.
239+
// See: https://github.com/ROCm/hip/issues/2253
240+
const auto blocks_x = utils::cuda::cap_grid_dim_x_from_workload(
241+
sorted_linear_indices_run.numel(),
242+
threads,
243+
at::cuda::getCurrentCUDAStream());
244+
dim3 blocks(blocks_x, N);
224245
auto grad_weight = at::zeros_like(weight);
225246
AT_DISPATCH_INDEX_TYPES(
226247
indices.scalar_type(), "batched_unary_embeddings_backward_kernel", [&] {

fbgemm_gpu/test/batched_unary_embeddings_test.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
# pyre-strict
99

10+
# pyre-ignore-all-errors[56]
11+
1012

1113
import random
1214
import unittest
@@ -21,10 +23,10 @@
2123
from fbgemm_gpu import open_source # noqa: F401
2224

2325
# pyre-ignore[21]
24-
from test_utils import gpu_unavailable
26+
from test_utils import gpu_memory_lt_gb, gpu_unavailable
2527

2628
except Exception:
27-
from fbgemm_gpu.test.test_utils import gpu_unavailable
29+
from fbgemm_gpu.test.test_utils import gpu_memory_lt_gb, gpu_unavailable
2830

2931
torch.ops.load_library("//deeplearning/fbgemm/fbgemm_gpu:sparse_ops")
3032

@@ -236,6 +238,108 @@ def test_gpu(self) -> None:
236238
def test_cpu(self) -> None:
237239
self._test_main(gpu_infer=False)
238240

241+
@unittest.skipIf(*gpu_unavailable)
242+
# This test exercises the HIP launch-side limit and requires a large
243+
# output tensor (~17 GiB) plus offsets (~4 GiB) — total ~22 GiB GPU
244+
# peak. Most CI machines without 24+ GiB of HBM will skip this. ROCm
245+
# developer machines (MI300/MI350, 192/256 GiB HBM) run it.
246+
@unittest.skipIf(*gpu_memory_lt_gb(24))
247+
def test_batched_unary_embeddings_forward_large_grid(self) -> None:
248+
"""
249+
Reproduces the HIP grid-overflow bug in
250+
batched_unary_embeddings_forward_kernel and verifies output
251+
correctness via a downsampled CPU oracle.
252+
253+
With block size up to 512 and grid (cuda_calc_xblock_count(B, 512),
254+
T, N), total threads ~= B * T * N. For B*T*N > 2**32, total threads
255+
exceed the HIP 2**32 limit, causing FBGEMM_LAUNCH_KERNEL ->
256+
KernelLauncher::checkThreadCountNotExceeded to TORCH_CHECK-fail on
257+
ROCm pre-fix.
258+
259+
Verification strategy (per master plan's downsampled-oracle
260+
guidance for ops where the full-scale CPU oracle is impractical):
261+
262+
1. Full-scale invocation to verify launch survival at the
263+
cap-trip scale. Uses zero-length offsets so the kernel does no
264+
per-segment work; output shape and zero-fill are asserted.
265+
2. Small-scale invocation vs CPU dispatch to validate kernel
266+
correctness end-to-end at a scale where the CPU oracle output
267+
(~MB range) is cheap to compute and compare element-wise.
268+
"""
269+
270+
# The production cap is `blocks_x_capped = min(blocks_x_uncapped,
271+
# get_max_thread_blocks(stream))` where `get_max_thread_blocks =
272+
# 64 * #SMs ~= 16384` on MI300/MI350. For the cap to actually help,
273+
# we need:
274+
# (a) blocks_x_uncapped > 16384 (so cap applies) -> B > 16384*512.
275+
# (b) blocks_x_uncapped * T * N * 512 > 2**32 (pre-fix trips).
276+
# (c) 16384 * T * N * 512 < 2**32 (post-fix passes), i.e. T*N < 512.
277+
# Choose T=4, N=8 (T*N=32) and B = (1<<27)+1 = 2**27+1 so blocks_x =
278+
# ceil(B/512) = 2**18 + 1 (>> 16384), pre-fix total ~= 2**32 + 2**14
279+
# (trips), post-fix total = 16384*32*512 = 2**28 (well under 2**32).
280+
B = (1 << 27) + 1
281+
T = 4
282+
N = 8
283+
284+
device = torch.device(torch.accelerator.current_accelerator() or "cuda")
285+
286+
# ---- Step 1: full-scale launch survival (cap-trip detection). ----
287+
# weight[N, sum_E] where sum_E = T (each table has E=1). ~16 MB.
288+
weight_large = torch.zeros((N, T), dtype=torch.float32, device=device)
289+
table_offsets_large = torch.arange(T + 1, dtype=torch.int64, device=device)
290+
# offsets[T*B+1] = all-zero so each (t,b) has L=0; no per-segment work.
291+
offsets_large = torch.zeros(T * B + 1, dtype=torch.int64, device=device)
292+
indices_large = torch.empty(0, dtype=torch.int64, device=device)
293+
294+
output_large = torch.ops.fbgemm.batched_unary_embeddings(
295+
weight_large, table_offsets_large, offsets_large, indices_large
296+
)
297+
298+
self.assertEqual(output_large.shape, (N, B, T))
299+
self.assertTrue(torch.all(output_large == 0).item())
300+
del output_large
301+
302+
# ---- Step 2: downsampled CPU-oracle correctness check. ----
303+
# Same kernel code path, smaller scale to keep the CPU oracle cheap.
304+
small_B = 4
305+
small_T = 3
306+
307+
# Two embedding tables, each with E=2 rows. sum_E = 2*small_T = 6.
308+
# N=2 (rows of weight). Distinct values per (n, row) so any "kernel
309+
# addressed wrong row" bug surfaces.
310+
small_weight_cpu = torch.tensor(
311+
[
312+
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
313+
[10.0, 20.0, 30.0, 40.0, 50.0, 60.0],
314+
],
315+
dtype=torch.float32,
316+
)
317+
# table_offsets[t]: start row of table t in weight columns.
318+
small_table_offsets_cpu = torch.tensor([0, 2, 4, 6], dtype=torch.int64)
319+
# offsets[t*B+b+1] - offsets[t*B+b] = length of (t,b) segment.
320+
# Use 1 index per (t,b) cell. Total indices = T*B = 12.
321+
small_offsets_cpu = torch.arange(small_T * small_B + 1, dtype=torch.int64)
322+
# Each (t,b) picks row 0 of its table.
323+
small_indices_cpu = torch.zeros(small_T * small_B, dtype=torch.int64)
324+
325+
# CPU reference oracle — same op, different dispatch.
326+
small_output_cpu = torch.ops.fbgemm.batched_unary_embeddings(
327+
small_weight_cpu,
328+
small_table_offsets_cpu,
329+
small_offsets_cpu,
330+
small_indices_cpu,
331+
)
332+
333+
# GPU under test.
334+
small_output_gpu = torch.ops.fbgemm.batched_unary_embeddings(
335+
small_weight_cpu.to(device),
336+
small_table_offsets_cpu.to(device),
337+
small_offsets_cpu.to(device),
338+
small_indices_cpu.to(device),
339+
)
340+
341+
torch.testing.assert_close(small_output_gpu.cpu(), small_output_cpu)
342+
239343

240344
if __name__ == "__main__":
241345
unittest.main()

0 commit comments

Comments
 (0)