Skip to content

Commit 133e6dc

Browse files
levythufacebook-github-bot
authored andcommitted
Implement multi-pass prefetch for memory efficiency
Summary: ## Context Memory snapshot shows significant memory usage during prefetch kernels (specifically, `linearize_cache_index` and `lru_cache_populate`), which is estimated to be 6x of input size And unfortunately, due to they using dedicated stream, the memory cannot be reused by any other stream without performance penalty. So we need to lower down the peak prefetch memory usage as much as possible. ## MultiPass Prefetch (MPP) Multipass prefetch is basically a technique to sacrifice a bit of more running time for less peak memory during prefetch: We observed that intermediate memory usage for all functions during prefetch is `O(N)`, so we reduce the total prefetched index (`N`) for each pass to reduce the peak temporary usage. The following passes will recycle the memory used in the first pass so they won't further increase the memory footprint. **Benefit** With this being turned on, the peak memory usage will be dropped from `6 * input_size` to `(6 / M) * input_size`, where `M` is the total # of passes being configured. **Overhead** Overall, the bigger `M` we configured, the slower we'll be. But the overall overhead is acceptable. - **Efficiency regression**: Prefetch is taking longer because the process of cache lookup is being repeated for every duplicate index. In the past, they're deduped before being looked up, but now they might be look up multiple times if duplicate index are across different passes. - The regression is overall insignificant, as the major cost is the data movement between DDR and HBM. We'll always copy the data only once, even if they're duplicated across different passes. - The regression is likely hidden from the actual training performance, since prefetch happen in a separate stream. As long as it's not long enough to block sparse backward it's invisible. - **Spamming CUDA Launch Queue**: CUDA is allowing max # of 1024 pending kernels. CPU will go blocking if more are submitted. If a kernel is really small, we'll easily spam launch queue and greatly hurt QPS. We mitigate this via limit the minimal # of elements for a pass. ## What's in the patch? 1. Add multipass prefetch config to the interface of TBE. By default it's None for full backward compatibility 2. Modify the `lru_find_uncached` to make it idempotent -- if we tried to lock the same id multiple times in one single timestep (but multiple passes), we'll increase lock counter by only one. Differential Revision: D56908989
1 parent e83e81a commit 133e6dc

4 files changed

Lines changed: 188 additions & 64 deletions

File tree

fbgemm_gpu/fbgemm_gpu/split_table_batched_embeddings_ops_training.py

Lines changed: 163 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ class UVMCacheStatsIndex(enum.IntEnum):
143143
num_conflict_misses = 5
144144

145145

146+
@dataclass
147+
class MultiPassPrefetchConfig:
148+
# Number of passes to split indices tensor into. Actual number of passes may
149+
# be less if indices tensor is too small to split.
150+
num_passes: int = 12
151+
152+
# The minimal number of element in indices tensor to be able to splitted into
153+
# two passes. This is useful to prevent too many prefetch kernels spamming
154+
# the CUDA launch queue.
155+
# The default 6M indices means 6M * 8 * 6 = approx. 300MB of memory overhead
156+
# per pass.
157+
min_splitable_pass_size: int = 6 * 1024 * 1024
158+
159+
146160
def construct_split_state(
147161
embedding_specs: List[Tuple[int, int, EmbeddingLocation, ComputeDevice]],
148162
rowwise: bool,
@@ -390,6 +404,7 @@ def __init__( # noqa C901
390404
# Embedding table names that are contained in this TBE.
391405
table_names: Optional[List[str]] = None,
392406
optimizer_state_dtypes: Optional[Dict[str, SparseType]] = None,
407+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = None,
393408
) -> None:
394409
super(SplitTableBatchedEmbeddingBagsCodegen, self).__init__()
395410
self.uuid = str(uuid.uuid4())
@@ -403,12 +418,35 @@ def __init__( # noqa C901
403418
self.prefetch_pipeline: bool = prefetch_pipeline
404419
self.lock_cache_line: bool = self.prefetch_pipeline
405420
self.use_uniq_cache_locations_bwd: bool = self.prefetch_pipeline
421+
self.multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = (
422+
multipass_prefetch_config
423+
)
406424

407425
if record_cache_metrics is not None:
408426
self.record_cache_metrics = record_cache_metrics
409427
else:
410428
self.record_cache_metrics = RecordCacheMetrics(False, False)
411429

430+
if multipass_prefetch_config:
431+
assert (
432+
prefetch_pipeline
433+
), "Multipass prefetch makes no sense in non-prefetch mode."
434+
assert (
435+
cache_algorithm == CacheAlgorithm.LRU
436+
), "Multipass prefetch is only supported in LRU cache."
437+
assert (
438+
multipass_prefetch_config.num_passes > 0
439+
), f"num_passes must be positive, get {multipass_prefetch_config.num_passes}"
440+
assert (
441+
multipass_prefetch_config.min_splitable_pass_size > 0
442+
), f"min_splitable_pass_size must be positive, get {multipass_prefetch_config.min_splitable_pass_size}"
443+
assert (
444+
not self.record_cache_metrics.record_cache_miss_counter
445+
and not self.record_cache_metrics.record_tablewise_cache_miss
446+
), self.log(
447+
"Unique cache miss counters are not accurate in multipass prefetch and therefore not supported"
448+
)
449+
412450
self.embedding_specs = embedding_specs
413451
(rows, dims, locations, compute_devices) = zip(*embedding_specs)
414452
T_ = len(self.embedding_specs)
@@ -926,6 +964,48 @@ def _register_nonpersistent_buffers(self, prefix: str) -> None:
926964
persistent=False,
927965
)
928966

967+
@staticmethod
968+
def get_prefetch_passes(
969+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig],
970+
input_tensor: Tensor,
971+
output_tensor: Tensor,
972+
) -> List[Tuple[Tensor, Tensor, int]]:
973+
"""
974+
Given input (the indices to forward), return the segmentation for each pass
975+
in the format of (input[start_idx:end_idx], output[start_idx:end_idx], start_idx).
976+
977+
Caller should guarantee input and output are having the size on dimension 0
978+
The returned segments are guaranteed to completely and non-overlappingly cover the input tensor.
979+
980+
In non-multipass-prefetch mode, it returns the input/output tensor itself.
981+
"""
982+
if multipass_prefetch_config is None:
983+
return [(input_tensor, output_tensor, 0)]
984+
mpp_config: MultiPassPrefetchConfig = multipass_prefetch_config
985+
986+
N = input_tensor.size(0)
987+
if N <= mpp_config.num_passes or mpp_config.num_passes == 1:
988+
# One row per pass, just don't split
989+
return [(input_tensor, output_tensor, 0)]
990+
991+
pass_size: int = max(
992+
(N + mpp_config.num_passes - 1) // mpp_config.num_passes,
993+
mpp_config.min_splitable_pass_size,
994+
)
995+
ret: List[Tuple[Tensor, Tensor, int]] = []
996+
for i in range(0, N, pass_size):
997+
# start_idx must be less than end_idx
998+
start_idx = i
999+
end_idx = min(i + pass_size, N)
1000+
ret.append(
1001+
(
1002+
input_tensor[start_idx:end_idx],
1003+
output_tensor[start_idx:end_idx],
1004+
start_idx,
1005+
)
1006+
)
1007+
return ret
1008+
9291009
def get_states(self, prefix: str) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
9301010
if not hasattr(self, f"{prefix}_physical_placements"):
9311011
raise DoesNotHavePrefix()
@@ -1195,7 +1275,12 @@ def forward( # noqa: C901
11951275
self._report_tbe_mem_usage()
11961276

11971277
if len(self.timesteps_prefetched) == 0:
1198-
self._prefetch(indices, offsets, vbe_metadata)
1278+
# In forward, we don't enable multi-pass prefetch as we want the process
1279+
# to be as fast as possible and memory usage doesn't matter (will be recycled
1280+
# by dense fwd/bwd)
1281+
self._prefetch(
1282+
indices, offsets, vbe_metadata, multipass_prefetch_config=None
1283+
)
11991284

12001285
if len(self.timesteps_prefetched) > 0:
12011286
self.timesteps_prefetched.pop(0)
@@ -1510,7 +1595,12 @@ def prefetch(
15101595
offsets,
15111596
batch_size_per_feature_per_rank,
15121597
)
1513-
self._prefetch(indices, offsets, vbe_metadata)
1598+
self._prefetch(
1599+
indices,
1600+
offsets,
1601+
vbe_metadata,
1602+
multipass_prefetch_config=self.multipass_prefetch_config,
1603+
)
15141604
if forward_stream is not None:
15151605
self._prefetch_tensors_record_stream(forward_stream)
15161606

@@ -1519,6 +1609,7 @@ def _prefetch(
15191609
indices: Tensor,
15201610
offsets: Tensor,
15211611
vbe_metadata: Optional[invokers.lookup_args.VBEMetadata] = None,
1612+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = None,
15221613
) -> None:
15231614
if not is_torchdynamo_compiling():
15241615
# Mutations of nn.Module attr forces dynamo restart of Analysis which increases compilation time
@@ -1535,81 +1626,90 @@ def _prefetch(
15351626
self.local_uvm_cache_stats.zero_()
15361627
self._report_io_size_count("prefetch_input", indices)
15371628

1538-
linear_cache_indices = torch.ops.fbgemm.linearize_cache_indices(
1539-
self.cache_hash_size_cumsum,
1540-
indices,
1541-
offsets,
1542-
vbe_metadata.B_offsets if vbe_metadata is not None else None,
1543-
vbe_metadata.max_B if vbe_metadata is not None else -1,
1544-
)
1545-
1546-
if (
1547-
self.record_cache_metrics.record_cache_miss_counter
1548-
or self.record_cache_metrics.record_tablewise_cache_miss
1629+
final_lxu_cache_locations = torch.empty_like(indices, dtype=torch.int32)
1630+
for (
1631+
partial_indices,
1632+
partial_lxu_cache_locations,
1633+
base_offset,
1634+
) in self.get_prefetch_passes(
1635+
multipass_prefetch_config, indices, final_lxu_cache_locations
15491636
):
1550-
lxu_cache_locations = torch.ops.fbgemm.lxu_cache_lookup(
1551-
linear_cache_indices,
1552-
self.lxu_cache_state,
1553-
self.total_cache_hash_size,
1554-
self.gather_uvm_cache_stats,
1555-
self.local_uvm_cache_stats,
1637+
linear_cache_indices = torch.ops.fbgemm.linearize_cache_indices(
1638+
self.cache_hash_size_cumsum,
1639+
partial_indices,
1640+
offsets,
1641+
vbe_metadata.B_offsets if vbe_metadata is not None else None,
1642+
vbe_metadata.max_B if vbe_metadata is not None else -1,
1643+
base_offset,
15561644
)
1557-
if self.record_cache_metrics.record_cache_miss_counter:
1558-
self._update_cache_miss_counter(
1559-
lxu_cache_locations, linear_cache_indices
1645+
1646+
if (
1647+
self.record_cache_metrics.record_cache_miss_counter
1648+
or self.record_cache_metrics.record_tablewise_cache_miss
1649+
):
1650+
lxu_cache_locations = torch.ops.fbgemm.lxu_cache_lookup(
1651+
linear_cache_indices,
1652+
self.lxu_cache_state,
1653+
self.total_cache_hash_size,
1654+
self.gather_uvm_cache_stats,
1655+
self.local_uvm_cache_stats,
1656+
)
1657+
if self.record_cache_metrics.record_cache_miss_counter:
1658+
self._update_cache_miss_counter(
1659+
lxu_cache_locations, linear_cache_indices
1660+
)
1661+
if self.record_cache_metrics.record_tablewise_cache_miss:
1662+
self._update_tablewise_cache_miss(
1663+
lxu_cache_locations, linear_cache_indices, offsets
1664+
)
1665+
1666+
if self.cache_algorithm == CacheAlgorithm.LRU:
1667+
torch.ops.fbgemm.lru_cache_populate(
1668+
self.weights_uvm,
1669+
self.cache_hash_size_cumsum,
1670+
self.total_cache_hash_size,
1671+
self.cache_index_table_map,
1672+
self.weights_offsets,
1673+
self.D_offsets,
1674+
linear_cache_indices,
1675+
self.lxu_cache_state,
1676+
self.lxu_cache_weights,
1677+
self.timestep,
1678+
self.lxu_state,
1679+
self.stochastic_rounding,
1680+
self.gather_uvm_cache_stats,
1681+
self.local_uvm_cache_stats,
1682+
self.lock_cache_line,
1683+
self.lxu_cache_locking_counter,
15601684
)
1561-
if self.record_cache_metrics.record_tablewise_cache_miss:
1562-
self._update_tablewise_cache_miss(
1563-
lxu_cache_locations, linear_cache_indices, offsets
1685+
elif self.cache_algorithm == CacheAlgorithm.LFU:
1686+
torch.ops.fbgemm.lfu_cache_populate(
1687+
self.weights_uvm,
1688+
self.cache_hash_size_cumsum,
1689+
self.total_cache_hash_size,
1690+
self.cache_index_table_map,
1691+
self.weights_offsets,
1692+
self.D_offsets,
1693+
linear_cache_indices,
1694+
self.lxu_cache_state,
1695+
self.lxu_cache_weights,
1696+
self.lxu_state,
1697+
self.stochastic_rounding,
15641698
)
15651699

1566-
if self.cache_algorithm == CacheAlgorithm.LRU:
1567-
torch.ops.fbgemm.lru_cache_populate(
1568-
self.weights_uvm,
1569-
self.cache_hash_size_cumsum,
1570-
self.total_cache_hash_size,
1571-
self.cache_index_table_map,
1572-
self.weights_offsets,
1573-
self.D_offsets,
1700+
torch.ops.fbgemm.lxu_cache_lookup(
15741701
linear_cache_indices,
15751702
self.lxu_cache_state,
1576-
self.lxu_cache_weights,
1577-
self.timestep,
1578-
self.lxu_state,
1579-
self.stochastic_rounding,
1703+
self.total_cache_hash_size,
15801704
self.gather_uvm_cache_stats,
15811705
self.local_uvm_cache_stats,
1582-
self.lock_cache_line,
1583-
self.lxu_cache_locking_counter,
1584-
)
1585-
elif self.cache_algorithm == CacheAlgorithm.LFU:
1586-
torch.ops.fbgemm.lfu_cache_populate(
1587-
self.weights_uvm,
1588-
self.cache_hash_size_cumsum,
1589-
self.total_cache_hash_size,
1590-
self.cache_index_table_map,
1591-
self.weights_offsets,
1592-
self.D_offsets,
1593-
linear_cache_indices,
1594-
self.lxu_cache_state,
1595-
self.lxu_cache_weights,
1596-
self.lxu_state,
1597-
self.stochastic_rounding,
1706+
lxu_cache_locations_output=partial_lxu_cache_locations,
15981707
)
15991708

16001709
assert (
16011710
len(self.lxu_cache_locations_list) < self.max_prefetch_depth
16021711
), f"self.lxu_cache_locations_list has grown to size: {len(self.lxu_cache_locations_list)}, this exceeds the maximum: {self.max_prefetch_depth}. This probably indicates an error in logic where prefetch() is being called more frequently than forward()"
1603-
1604-
lxu_cache_locations = torch.ops.fbgemm.lxu_cache_lookup(
1605-
linear_cache_indices,
1606-
self.lxu_cache_state,
1607-
self.total_cache_hash_size,
1608-
self.gather_uvm_cache_stats,
1609-
self.local_uvm_cache_stats,
1610-
)
1611-
1612-
self.lxu_cache_locations_list.append(lxu_cache_locations)
1712+
self.lxu_cache_locations_list.append(final_lxu_cache_locations)
16131713

16141714
if self.gather_uvm_cache_stats:
16151715
# Accumulate local_uvm_cache_stats (int32) into uvm_cache_stats (int64).

fbgemm_gpu/src/split_embeddings_cache/lru_cache_find.cu

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,11 @@ __global__ __launch_bounds__(kMaxThreads) void lru_cache_find_uncached_kernel(
124124
const bool found = ::__ldg((&lxu_cache_state[cache_set][0]) + slot) == idx;
125125
if (found) {
126126
// mark it as recently accessed so we don't evict.
127+
const bool already_locked = lru_state[cache_set][slot] == time_stamp;
127128
lru_state[cache_set][slot] = time_stamp;
128-
if (lock_cache_line) {
129+
// Don't lock the line one more time if we have locked it in the same
130+
// batch (timestamp)
131+
if (lock_cache_line && !already_locked) {
129132
lxu_cache_locking_counter[cache_set][slot] += 1;
130133
}
131134
}

fbgemm_gpu/test/tbe/cache/cache_common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
)
2525
from fbgemm_gpu.split_table_batched_embeddings_ops_training import (
2626
ComputeDevice,
27+
MultiPassPrefetchConfig,
2728
SplitTableBatchedEmbeddingBagsCodegen,
2829
)
2930

@@ -96,6 +97,7 @@ def generate_cache_tbes(
9697
stochastic_rounding: bool = False,
9798
gather_uvm_cache_stats: bool = False,
9899
reporter_config: Optional[TestingStatsReporterConfig] = None,
100+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = None,
99101
) -> Tuple[
100102
SplitTableBatchedEmbeddingBagsCodegen,
101103
SplitTableBatchedEmbeddingBagsCodegen,
@@ -153,6 +155,7 @@ def generate_cache_tbes(
153155
cache_precision=weights_cache_precision,
154156
gather_uvm_cache_stats=gather_uvm_cache_stats,
155157
stats_reporter_config=reporter_config,
158+
multipass_prefetch_config=multipass_prefetch_config,
156159
)
157160

158161
if use_int_weight:

0 commit comments

Comments
 (0)