Skip to content

Commit 2db16a8

Browse files
q10meta-codesync[bot]
authored andcommitted
Add grid-stride + ROCm cap to keyed_jagged_index_select_dim1_kernel and keyed_jagged_index_add_dim1_kernel; fast-fail TORCH_CHECK for index_select_scalar_cumsum_kernel (#6025)
Summary: Pull Request resolved: #6025 X-link: https://github.com/facebookresearch/FBGEMM/pull/2930 Diff 10/10 (final) of the Tier-2 ROCm grid-overflow fix stack. Three kernels in keyed_jagged_index_select_dim1.cu need attention: - keyed_jagged_index_select_dim1_kernel (forward) and keyed_jagged_index_add_dim1_kernel (backward) both used flat if (tid < N) predication. This diff wraps each in an explicit int64_t grid-stride for-loop and adds the standard #ifdef USE_ROCM host-side cap on grid_size at both launch sites. - index_select_scalar_cumsum_kernel is a cross-block prefix-sum with block_flags / block_sums coordination. Capping the grid would corrupt the cumsum. Instead, this diff adds a fast-fail TORCH_CHECK on the host side before the launch, requiring num_output_lengths < 2^32. The full algorithmic restructure (per-segment block tiling or two-pass scan via cub::DeviceScan) is tracked as a Tier-3 follow-up. Reviewed By: henrylhtsang Differential Revision: D105205634 fbshipit-source-id: 852630d2c4d3e9ad5e3843b5031443ca99723943
1 parent c32d210 commit 2db16a8

2 files changed

Lines changed: 122 additions & 6 deletions

File tree

fbgemm_gpu/src/jagged_tensor_ops/keyed_jagged_index_select_dim1.cu

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,15 @@ __global__ void keyed_jagged_index_select_dim1_kernel(
169169
output_offsets,
170170
const int num_batches,
171171
const int input_batch_size) {
172-
const int64_t tid = blockIdx.x * blockDim.x + threadIdx.x;
173172
const int output_batch_size = indices.size(0);
174173
const int64_t num_outputs = output.size(0);
175174

176-
if (tid < num_outputs) {
175+
// Grid-stride over outputs so a capped grid (used on ROCm to avoid the
176+
// 2^32 launch-side limit) still covers every output element.
177+
for (int64_t tid =
178+
static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
179+
tid < num_outputs;
180+
tid += static_cast<int64_t>(gridDim.x) * blockDim.x) {
177181
entry_t index_pos;
178182
const entry_t num_entries =
179183
static_cast<entry_t>(num_batches) * output_batch_size;
@@ -220,11 +224,15 @@ __global__ void keyed_jagged_index_add_dim1_kernel(
220224
output_offsets,
221225
const int num_batches,
222226
const int output_batch_size) {
223-
const int64_t tid = blockIdx.x * blockDim.x + threadIdx.x;
224227
const int input_batch_size = indices.size(0);
225228
const int64_t num_inputs = input.size(0);
226229

227-
if (tid < num_inputs) {
230+
// Grid-stride over inputs so a capped grid (used on ROCm to avoid the
231+
// 2^32 launch-side limit) still covers every input element.
232+
for (int64_t tid =
233+
static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
234+
tid < num_inputs;
235+
tid += static_cast<int64_t>(gridDim.x) * blockDim.x) {
228236
entry_t index_pos;
229237
const entry_t num_entries =
230238
static_cast<entry_t>(num_batches) * input_batch_size;
@@ -278,6 +286,20 @@ class KeyedJaggedIndexSelectDim1GPUOp
278286
" * indices.numel()=",
279287
indices.numel(),
280288
" causes overflow.");
289+
// Special: index_select_scalar_cumsum_kernel uses a cross-block
290+
// decoupled-look-back scan with block_flags / block_sums; capping the
291+
// grid would corrupt the cumsum. As a Tier-2 stop-gap, fast-fail with
292+
// TORCH_CHECK if the launch would trip the HIP 2^32 limit. Algorithmic
293+
// restructure (cub::DeviceScan or per-segment block tiling) is tracked
294+
// as a Tier-3 follow-up.
295+
TORCH_CHECK(
296+
static_cast<uint64_t>(num_output_lengths) <
297+
(static_cast<uint64_t>(1) << 32),
298+
"index_select_scalar_cumsum_kernel: num_output_lengths (",
299+
num_output_lengths,
300+
") must be < 2^32 to avoid HIP grid-overflow. The kernel uses a "
301+
"cross-block prefix scan that cannot be naively grid-strided. See "
302+
"Tier-3 follow-up for restructure.");
281303
auto grid_size = cuda_calc_xblock_count(
282304
num_output_lengths, MAX_CUMSUM_ENTRIES_PER_BLOCK);
283305
TORCH_CHECK_VALUE(
@@ -453,7 +475,12 @@ class KeyedJaggedIndexSelectDim1GPUOp
453475
if (weights.has_value()) {
454476
output_weights = at::empty({num_outputs}, weights.value().options());
455477
}
456-
grid_size = cuda_calc_xblock_count(num_outputs, kMaxThreads);
478+
// HIP enforces a hard limit of 2^32 total threads per launch.
479+
// keyed_jagged_index_select_dim1_kernel grid-strides over outputs, so
480+
// capping is correctness-preserving.
481+
// See: https://github.com/ROCm/hip/issues/2253
482+
grid_size = utils::cuda::cap_grid_dim_x_from_workload(
483+
num_outputs, kMaxThreads, at::cuda::getCurrentCUDAStream());
457484

458485
// output_offsets has to be contiguous because it is passed to
459486
// binary_search_range which takes raw pointers as arguments
@@ -633,7 +660,12 @@ class KeyedJaggedIndexSelectDim1GPUOp
633660
device_guard.set_index(grad.get_device());
634661

635662
Tensor grad_input = at::zeros({num_outputs}, grad.options());
636-
auto grid_size = cuda_calc_xblock_count(grad.numel(), kMaxThreads);
663+
// HIP enforces a hard limit of 2^32 total threads per launch.
664+
// keyed_jagged_index_add_dim1_kernel grid-strides over inputs, so
665+
// capping is correctness-preserving.
666+
// See: https://github.com/ROCm/hip/issues/2253
667+
auto grid_size = utils::cuda::cap_grid_dim_x_from_workload(
668+
grad.numel(), kMaxThreads, at::cuda::getCurrentCUDAStream());
637669
// grad_offsets has to be contiguous because it is passed to
638670
// binary_search_range which takes raw pointers as arguments
639671
const auto grad_offsets_contig = grad_offsets.expect_contiguous();

fbgemm_gpu/test/jagged/keyed_jagged_index_select_test.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,90 @@ def test_keyed_jagged_index_select_dim1_int32_overflow(
347347
atol=1e-2 if jagged_tensor_dtype in [torch.half, torch.bfloat16] else None,
348348
)
349349

350+
@optests.dontGenerateOpCheckTests(
351+
"large-grid GPU-memory-gated stress repro; opcheck variants add no op "
352+
"coverage and only skip on CPU samples (T191384137)"
353+
)
354+
@unittest.skipUnless(torch.cuda.is_available(), "GPU not available")
355+
def test_keyed_jagged_index_select_dim1_large_grid(self) -> None:
356+
"""
357+
Regression test for the HIP grid-overflow bug in
358+
``keyed_jagged_index_select_dim1_kernel`` and
359+
``keyed_jagged_index_add_dim1_kernel`` (D105205634 / Subplan D
360+
Diff #30).
361+
362+
Both kernels grid-stride over outputs/inputs respectively. The
363+
production fix caps grid_x to ``get_max_thread_blocks(stream)``
364+
on ROCm; on CUDA the grid still covers every element in a single
365+
stride. Either way the refactor must remain correctness-
366+
preserving. ``keyed_jagged_index_select_dim1`` is a CUDA-only op
367+
(no CPU kernel), so verification is end-to-end forward + backward
368+
correctness on the accelerator against a per-key
369+
``jagged_index_select`` reference, using sentinel non-zero
370+
lengths at a multi-key scale.
371+
372+
The third kernel touched by this diff —
373+
``index_select_scalar_cumsum_kernel`` — uses a cross-block
374+
decoupled-look-back scan and was given a Tier-C fast-fail
375+
TORCH_CHECK guard rather than a grid-stride retrofit. That
376+
Tier-C branch is exercised by code review of the diff itself
377+
(the production TORCH_CHECK line) and not unit-testable here
378+
without allocating a ~16 GiB indices tensor.
379+
"""
380+
device = torch.accelerator.current_accelerator()
381+
num_batches = 2
382+
input_batch_size = 32
383+
# Sparse sentinel non-zero lengths spread across batches.
384+
lengths = torch.zeros(
385+
num_batches * input_batch_size, dtype=torch.int64, device=device
386+
)
387+
lengths[0] = 3
388+
lengths[input_batch_size + input_batch_size // 2] = 2
389+
lengths[-1] = 4
390+
offsets = torch.concat(
391+
[torch.zeros(1, dtype=torch.long, device=device), lengths.cumsum(0)]
392+
)
393+
total = int(offsets[-1].item())
394+
395+
# Distinct values across rows; indices select all batches.
396+
values_init = torch.arange(total, dtype=torch.float32, device=device)
397+
indices = torch.arange(input_batch_size, dtype=torch.int64, device=device)
398+
399+
# Op under test: forward + backward on the accelerator.
400+
values = values_init.detach().clone().requires_grad_(True)
401+
out = torch.ops.fbgemm.keyed_jagged_index_select_dim1(
402+
values,
403+
lengths,
404+
offsets,
405+
indices,
406+
input_batch_size,
407+
)[0]
408+
409+
# Reference: keyed_jagged_index_select_dim1 has no CPU kernel, so build
410+
# the expected result on-device from the per-key jagged_index_select
411+
# primitive (same equivalence the other tests in this file rely on).
412+
values_ref = values_init.detach().clone().requires_grad_(True)
413+
output_ref = []
414+
for k in range(num_batches):
415+
key_lengths = lengths[k * input_batch_size : (k + 1) * input_batch_size]
416+
start_offset = offsets[k * input_batch_size]
417+
end_offset = offsets[(k + 1) * input_batch_size]
418+
key_values = values_ref[start_offset:end_offset].view(-1, 1)
419+
output_ref.append(
420+
torch.ops.fbgemm.jagged_index_select(key_values, key_lengths, indices)[
421+
0
422+
].view(-1)
423+
)
424+
output_ref = torch.concat(output_ref)
425+
426+
torch.testing.assert_close(out, output_ref)
427+
428+
out.sum().backward()
429+
output_ref.sum().backward()
430+
assert values.grad is not None
431+
assert values_ref.grad is not None
432+
torch.testing.assert_close(values.grad, values_ref.grad)
433+
350434

351435
if __name__ == "__main__":
352436
unittest.main()

0 commit comments

Comments
 (0)