Skip to content

Commit e954795

Browse files
stashuk-olekfacebook-github-bot
authored andcommitted
Fuse Triton TBE forward bounds checking (#4485)
Summary: Now that both of cuda/triton does it bounds check, we fuse that to avoid traffic. Adding as an option Reviewed By: TroyGarden, axeisghost Differential Revision: D114284802
1 parent 2c20ceb commit e954795

1 file changed

Lines changed: 221 additions & 32 deletions

File tree

torchrec/distributed/triton_tbe/triton_table_batched_embeddings.py

Lines changed: 221 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import torch
2020
import triton # @manual
2121
import triton.language as tl # @manual
22+
from fbgemm_gpu.config import FeatureGateName
2223
from fbgemm_gpu.split_embedding_configs import EmbOptimType as OptimType, SparseType
2324
from fbgemm_gpu.split_table_batched_embeddings_ops_common import (
2425
BoundsCheckMode,
@@ -77,6 +78,67 @@ def lengths_to_offsets(lengths: List[int], keep_last: bool = False) -> List[int]
7778
return offsets
7879

7980

81+
@triton.jit
82+
def _bounds_check_offsets_kernel(
83+
offsets_ptr,
84+
warning_ptr,
85+
num_indices,
86+
total_B,
87+
BLOCK_SIZE: tl.constexpr,
88+
) -> None:
89+
positions = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
90+
active = positions < total_B
91+
starts = tl.load(offsets_ptr + positions, mask=active, other=0)
92+
raw_ends = tl.load(offsets_ptr + positions + 1, mask=active, other=0)
93+
ends = tl.where(positions == total_B - 1, num_indices, raw_ends)
94+
invalid = active & ((starts < 0) | (starts > ends) | (ends > num_indices))
95+
if tl.program_id(0) == 0:
96+
last_offset = tl.load(offsets_ptr + total_B)
97+
if last_offset != num_indices:
98+
tl.atomic_add(warning_ptr, 1)
99+
100+
warning_count = tl.sum(invalid.to(tl.int64))
101+
if warning_count > 0:
102+
tl.atomic_add(warning_ptr, warning_count)
103+
104+
105+
@triton.jit
106+
def _repair_offsets_kernel(
107+
offsets_ptr,
108+
warning_ptr,
109+
num_indices,
110+
total_B,
111+
) -> None:
112+
if tl.load(warning_ptr) > 0:
113+
current = tl.maximum(0, tl.minimum(tl.load(offsets_ptr), num_indices))
114+
tl.store(offsets_ptr, current)
115+
position = 0
116+
while position < total_B:
117+
raw_end = tl.load(offsets_ptr + position + 1)
118+
raw_end = tl.where(position == total_B - 1, num_indices, raw_end)
119+
current = tl.maximum(current, tl.minimum(raw_end, num_indices))
120+
tl.store(offsets_ptr + position + 1, current)
121+
position += 1
122+
123+
124+
@triton.jit
125+
def _load_checked_index(
126+
indices_ptr,
127+
position,
128+
num_rows,
129+
mask,
130+
FUSED_BOUNDS_CHECK: tl.constexpr,
131+
):
132+
row_idx = tl.load(indices_ptr + position, mask=mask, other=0)
133+
if FUSED_BOUNDS_CHECK:
134+
invalid = mask & (row_idx != -1) & ((row_idx < 0) | (row_idx >= num_rows))
135+
tl.store(indices_ptr + position, 0, mask=invalid)
136+
row_idx = tl.where(invalid, 0, row_idx)
137+
else:
138+
invalid = row_idx < row_idx
139+
return row_idx, invalid
140+
141+
80142
@triton.jit
81143
def table_batched_embedding_bag_forward_weighted_kernel(
82144
output_ptr,
@@ -196,6 +258,8 @@ def table_batched_embedding_bag_forward_unweighted_kernel(
196258
embedding_dims_ptr,
197259
embedding_offsets_ptr,
198260
feature_table_map_ptr,
261+
rows_cumsum_ptr,
262+
bounds_check_warning_ptr,
199263
# VBE-specific pointers (only used when vbe=True)
200264
# pyre-fixme[2]: Parameter must be annotated.
201265
row_output_offsets_ptr,
@@ -206,10 +270,12 @@ def table_batched_embedding_bag_forward_unweighted_kernel(
206270
T: tl.constexpr,
207271
BLOCK_SIZE: tl.constexpr,
208272
vbe: tl.constexpr = False,
273+
FUSED_BOUNDS_CHECK: tl.constexpr = False,
209274
) -> None:
210275

211276
b = tl.program_id(0).to(tl.int64)
212277
col_offsets = tl.arange(0, BLOCK_SIZE)
278+
warning_count = 0
213279
if vbe:
214280
output_row_base = output_ptr # unused, VBE uses row_output_offsets
215281
else:
@@ -233,6 +299,10 @@ def table_batched_embedding_bag_forward_unweighted_kernel(
233299
table_offset = tl.load(table_offsets_ptr + table_idx)
234300
# embedding_dim and embedding_offset are indexed by feature
235301
embedding_dim = tl.load(embedding_dims_ptr + t)
302+
if FUSED_BOUNDS_CHECK:
303+
num_rows = tl.load(rows_cumsum_ptr + table_idx + 1) - tl.load(
304+
rows_cumsum_ptr + table_idx
305+
)
236306

237307
start = tl.load(offsets_ptr + b_t)
238308
end = tl.load(offsets_ptr + b_t + 1)
@@ -245,10 +315,41 @@ def table_batched_embedding_bag_forward_unweighted_kernel(
245315
endn = start + step * ns
246316

247317
for idx in range(start, endn, step):
248-
row_idx_0 = tl.load(indices_ptr + idx + 0)
249-
row_idx_1 = tl.load(indices_ptr + idx + 1)
250-
row_idx_2 = tl.load(indices_ptr + idx + 2)
251-
row_idx_3 = tl.load(indices_ptr + idx + 3)
318+
row_idx_0, invalid_0 = _load_checked_index(
319+
indices_ptr,
320+
idx + 0,
321+
num_rows if FUSED_BOUNDS_CHECK else 0,
322+
True,
323+
FUSED_BOUNDS_CHECK,
324+
)
325+
row_idx_1, invalid_1 = _load_checked_index(
326+
indices_ptr,
327+
idx + 1,
328+
num_rows if FUSED_BOUNDS_CHECK else 0,
329+
True,
330+
FUSED_BOUNDS_CHECK,
331+
)
332+
row_idx_2, invalid_2 = _load_checked_index(
333+
indices_ptr,
334+
idx + 2,
335+
num_rows if FUSED_BOUNDS_CHECK else 0,
336+
True,
337+
FUSED_BOUNDS_CHECK,
338+
)
339+
row_idx_3, invalid_3 = _load_checked_index(
340+
indices_ptr,
341+
idx + 3,
342+
num_rows if FUSED_BOUNDS_CHECK else 0,
343+
True,
344+
FUSED_BOUNDS_CHECK,
345+
)
346+
if FUSED_BOUNDS_CHECK:
347+
warning_count += (
348+
invalid_0.to(tl.int32)
349+
+ invalid_1.to(tl.int32)
350+
+ invalid_2.to(tl.int32)
351+
+ invalid_3.to(tl.int32)
352+
)
252353

253354
row_start_ptr_0 = weight_ptr + table_offset + row_idx_0 * embedding_dim
254355
row_start_ptr_1 = weight_ptr + table_offset + row_idx_1 * embedding_dim
@@ -273,7 +374,15 @@ def table_batched_embedding_bag_forward_unweighted_kernel(
273374
)
274375

275376
for idx in range(endn, end):
276-
row_idx = tl.load(indices_ptr + idx)
377+
row_idx, invalid = _load_checked_index(
378+
indices_ptr,
379+
idx,
380+
num_rows if FUSED_BOUNDS_CHECK else 0,
381+
True,
382+
FUSED_BOUNDS_CHECK,
383+
)
384+
if FUSED_BOUNDS_CHECK:
385+
warning_count += invalid.to(tl.int32)
277386
row_start_ptr = weight_ptr + table_offset + row_idx * embedding_dim
278387
row_ptrs = row_start_ptr + col_offsets
279388
row = tl.load(row_ptrs, mask=mask, other=0)
@@ -288,6 +397,9 @@ def table_batched_embedding_bag_forward_unweighted_kernel(
288397
bag_output_original = bag_output.to(tl.float32)
289398
tl.store(output_row_ptrs, bag_output_original, mask=mask)
290399

400+
if FUSED_BOUNDS_CHECK and warning_count > 0:
401+
tl.atomic_add(bounds_check_warning_ptr, warning_count.to(tl.int64))
402+
291403

292404
@triton.jit
293405
def triton_tbe_backward_short_run_unweighted(
@@ -1041,6 +1153,8 @@ def forward(
10411153
precomputed_total_B: int = 0,
10421154
precomputed_max_B: int = 0,
10431155
hoist_transpose_to_forward: bool = True,
1156+
bounds_check_warning: Optional[torch.Tensor] = None,
1157+
fused_bounds_check: bool = False,
10441158
) -> torch.Tensor:
10451159
# VBE support: use pre-computed metadata if available, otherwise compute
10461160
vbe = batch_size_per_feature_per_rank is not None
@@ -1120,6 +1234,15 @@ def forward(
11201234
(B, total_embedding_dim), device=weight.device, dtype=output_dtype
11211235
)
11221236

1237+
weighted = per_sample_weights is not None and per_sample_weights.numel() > 0
1238+
if fused_bounds_check and (
1239+
bounds_check_warning is None
1240+
or hoist_transpose_to_forward
1241+
or weighted
1242+
or vbe
1243+
):
1244+
raise ValueError("Invalid fused bounds-check configuration")
1245+
11231246
# For VBE backward, save row_output_offsets, B_offsets, and b_t_map
11241247
if vbe:
11251248
assert vbe_metadata.B_offsets is not None
@@ -1204,8 +1327,9 @@ def forward(
12041327

12051328
num_warps = 1
12061329

1207-
weighted = per_sample_weights is not None and per_sample_weights.numel() > 0
1208-
1330+
bounds_check_warning_ptr = (
1331+
bounds_check_warning if bounds_check_warning is not None else indices
1332+
)
12091333
# Prepare VBE pointers (use dummy tensor if not VBE)
12101334
row_output_offsets_ptr = (
12111335
row_output_offsets
@@ -1244,29 +1368,47 @@ def forward(
12441368
num_warps=num_warps,
12451369
)
12461370
else:
1247-
fwd_kernel = (
1248-
_amd_fwd_unweighted_kernel
1249-
if is_amd()
1250-
else table_batched_embedding_bag_forward_unweighted_kernel
1251-
)
1252-
fwd_kernel[(B,)](
1253-
output,
1254-
indices,
1255-
offsets,
1256-
weight,
1257-
table_offsets,
1258-
embedding_dims,
1259-
embedding_offsets,
1260-
feature_table_map,
1261-
row_output_offsets_ptr,
1262-
B_offsets_ptr,
1263-
total_embedding_dim,
1264-
B,
1265-
T,
1266-
BLOCK_SIZE=block_size,
1267-
vbe=vbe,
1268-
num_warps=num_warps,
1269-
)
1371+
if is_amd():
1372+
_amd_fwd_unweighted_kernel[(B,)](
1373+
output,
1374+
indices,
1375+
offsets,
1376+
weight,
1377+
table_offsets,
1378+
embedding_dims,
1379+
embedding_offsets,
1380+
feature_table_map,
1381+
row_output_offsets_ptr,
1382+
B_offsets_ptr,
1383+
total_embedding_dim,
1384+
B,
1385+
T,
1386+
BLOCK_SIZE=block_size,
1387+
vbe=vbe,
1388+
num_warps=num_warps,
1389+
)
1390+
else:
1391+
table_batched_embedding_bag_forward_unweighted_kernel[(B,)](
1392+
output,
1393+
indices,
1394+
offsets,
1395+
weight,
1396+
table_offsets,
1397+
embedding_dims,
1398+
embedding_offsets,
1399+
feature_table_map,
1400+
rows_cumsum,
1401+
bounds_check_warning_ptr,
1402+
row_output_offsets_ptr,
1403+
B_offsets_ptr,
1404+
total_embedding_dim,
1405+
B,
1406+
T,
1407+
BLOCK_SIZE=block_size,
1408+
vbe=vbe,
1409+
FUSED_BOUNDS_CHECK=fused_bounds_check,
1410+
num_warps=num_warps,
1411+
)
12701412

12711413
# Record a CUDA event to mark forward kernel completion.
12721414
# This is needed for synchronization before NCCL collectives.
@@ -1762,6 +1904,8 @@ def backward(ctx, dout) -> Tuple[None, ...]:
17621904
None, # precomputed_total_B
17631905
None, # precomputed_max_B
17641906
None, # hoist_transpose_to_forward
1907+
None,
1908+
None,
17651909
)
17661910

17671911

@@ -1783,6 +1927,7 @@ def __init__(
17831927
optimizer: OptimType = OptimType.EXACT_SGD,
17841928
device: Optional[torch.device] = None,
17851929
hoist_transpose_to_forward: bool = False,
1930+
fused_bounds_check: bool = False,
17861931
) -> None:
17871932
super().__init__()
17881933
logging.info("TritonTableBatchedEmbeddingBags init args: %s", locals())
@@ -1865,6 +2010,7 @@ def __init__(
18652010
self.eps = eps
18662011
self.optimizer = optimizer
18672012
self.hoist_transpose_to_forward = hoist_transpose_to_forward
2013+
self.fused_bounds_check = fused_bounds_check
18682014

18692015
# Initialize optimizer state
18702016
rows = [spec[0] for spec in embedding_specs]
@@ -1892,6 +2038,9 @@ def __init__(
18922038
)
18932039
self.bounds_check_warning = torch.tensor([0], device=device, dtype=torch.int64)
18942040
self.bounds_check_mode: BoundsCheckMode = BoundsCheckMode.V2_WARNING
2041+
self._disable_offsets_adjustment = (
2042+
FeatureGateName.DISABLE_OFFSETS_ADJUSTMENT.is_enabled()
2043+
)
18952044

18962045
def _bounds_check_config(self) -> Tuple[BoundsCheckMode, int]:
18972046
is_v2 = self.bounds_check_mode.name.startswith("V2_")
@@ -2030,9 +2179,25 @@ def forward(
20302179
offsets, batch_size_per_feature_per_rank
20312180
)
20322181

2033-
# Bounds check (VBE-aware)
20342182
bounds_check_mode, bounds_check_version = self._bounds_check_config()
2035-
if bounds_check_mode != BoundsCheckMode.NONE:
2183+
use_fused_bounds_check = (
2184+
self.fused_bounds_check
2185+
and bounds_check_mode == BoundsCheckMode.WARNING
2186+
and batch_size_per_feature_per_rank is None
2187+
and (per_sample_weights is None or per_sample_weights.numel() == 0)
2188+
and not self.hoist_transpose_to_forward
2189+
and not is_amd()
2190+
)
2191+
2192+
if use_fused_bounds_check:
2193+
if indices.dim() != 1 or offsets.dim() != 1:
2194+
raise RuntimeError("indices and offsets must be one-dimensional")
2195+
if offsets.numel() == 0 or (offsets.numel() - 1) % self.T != 0:
2196+
raise RuntimeError("offsets size must equal B * T + 1")
2197+
if indices.device != offsets.device or indices.device != self.weight.device:
2198+
raise RuntimeError("TBE inputs must be on the same device")
2199+
2200+
if bounds_check_mode != BoundsCheckMode.NONE and not use_fused_bounds_check:
20362201
torch.ops.fbgemm.bounds_check_indices(
20372202
self.rows_per_table,
20382203
indices,
@@ -2047,6 +2212,28 @@ def forward(
20472212
info_B_mask=info_B_mask if info_B_mask > 0 else -1,
20482213
bounds_check_version=bounds_check_version,
20492214
)
2215+
elif use_fused_bounds_check:
2216+
self.bounds_check_warning.zero_()
2217+
total_bags = offsets.size(0) - 1
2218+
if total_bags > 0:
2219+
_bounds_check_offsets_kernel[(triton.cdiv(total_bags, 256),)](
2220+
offsets,
2221+
self.bounds_check_warning,
2222+
indices.numel(),
2223+
total_bags,
2224+
BLOCK_SIZE=256,
2225+
num_warps=8,
2226+
)
2227+
if self._disable_offsets_adjustment:
2228+
torch._assert_async(self.bounds_check_warning == 0)
2229+
else:
2230+
_repair_offsets_kernel[(1,)](
2231+
offsets,
2232+
self.bounds_check_warning,
2233+
indices.numel(),
2234+
total_bags,
2235+
num_warps=1,
2236+
)
20502237

20512238
return TritonTBE.apply(
20522239
indices,
@@ -2082,6 +2269,8 @@ def forward(
20822269
total_B,
20832270
max_B,
20842271
self.hoist_transpose_to_forward,
2272+
self.bounds_check_warning,
2273+
use_fused_bounds_check,
20852274
)
20862275

20872276
def split_embedding_weights(self) -> List[torch.Tensor]:

0 commit comments

Comments
 (0)