Skip to content

Commit 3e2f269

Browse files
levythufacebook-github-bot
authored andcommitted
Implement multi-pass prefetch for memory efficiency (pytorch#2566)
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. Reviewed By: sryap Differential Revision: D56908989
1 parent 770603b commit 3e2f269

5 files changed

Lines changed: 234 additions & 64 deletions

File tree

fbgemm_gpu/fbgemm_gpu/split_table_batched_embeddings_ops_common.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ class CacheAlgorithm(enum.Enum):
3636
LFU = 1
3737

3838

39+
class MultiPassPrefetchConfig(NamedTuple):
40+
# Number of passes to split indices tensor into. Actual number of passes may
41+
# be less if indices tensor is too small to split.
42+
num_passes: int = 12
43+
44+
# The minimal number of element in indices tensor to be able to split into
45+
# two passes. This is useful to prevent too many prefetch kernels spamming
46+
# the CUDA launch queue.
47+
# The default 6M indices means 6M * 8 * 6 = approx. 300MB of memory overhead
48+
# per pass.
49+
min_splitable_pass_size: int = 6 * 1024 * 1024
50+
51+
3952
class PoolingMode(enum.IntEnum):
4053
SUM = 0
4154
MEAN = 1

fbgemm_gpu/fbgemm_gpu/split_table_batched_embeddings_ops_training.py

Lines changed: 143 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
construct_cache_state,
4141
EmbeddingLocation,
4242
MAX_PREFETCH_DEPTH,
43+
MultiPassPrefetchConfig,
4344
PoolingMode,
4445
RecordCacheMetrics,
4546
SplitState,
@@ -392,6 +393,7 @@ def __init__( # noqa C901
392393
# Embedding table names that are contained in this TBE.
393394
table_names: Optional[List[str]] = None,
394395
optimizer_state_dtypes: Optional[Dict[str, SparseType]] = None,
396+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = None,
395397
) -> None:
396398
super(SplitTableBatchedEmbeddingBagsCodegen, self).__init__()
397399
self.uuid = str(uuid.uuid4())
@@ -405,12 +407,33 @@ def __init__( # noqa C901
405407
self.prefetch_pipeline: bool = prefetch_pipeline
406408
self.lock_cache_line: bool = self.prefetch_pipeline
407409
self.use_uniq_cache_locations_bwd: bool = self.prefetch_pipeline
410+
self.multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = (
411+
multipass_prefetch_config
412+
)
408413

409414
if record_cache_metrics is not None:
410415
self.record_cache_metrics = record_cache_metrics
411416
else:
412417
self.record_cache_metrics = RecordCacheMetrics(False, False)
413418

419+
if multipass_prefetch_config:
420+
assert (
421+
prefetch_pipeline
422+
), "Multipass prefetch makes no sense in non-prefetch mode."
423+
assert (
424+
cache_algorithm == CacheAlgorithm.LRU
425+
), "Multipass prefetch is only supported in LRU cache."
426+
assert (
427+
multipass_prefetch_config.num_passes > 0
428+
), f"num_passes must be positive, get {multipass_prefetch_config.num_passes}"
429+
assert (
430+
multipass_prefetch_config.min_splitable_pass_size > 0
431+
), f"min_splitable_pass_size must be positive, get {multipass_prefetch_config.min_splitable_pass_size}"
432+
assert (
433+
not self.record_cache_metrics.record_cache_miss_counter
434+
and not self.record_cache_metrics.record_tablewise_cache_miss
435+
), "Unique cache miss counters are not accurate in multipass prefetch and therefore not supported"
436+
414437
self.embedding_specs = embedding_specs
415438
(rows, dims, locations, compute_devices) = zip(*embedding_specs)
416439
T_ = len(self.embedding_specs)
@@ -928,6 +951,43 @@ def _register_nonpersistent_buffers(self, prefix: str) -> None:
928951
persistent=False,
929952
)
930953

954+
@staticmethod
955+
def get_prefetch_passes(
956+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig],
957+
input_tensor: Tensor,
958+
output_tensor: Tensor,
959+
) -> List[Tuple[Tensor, Tensor, int]]:
960+
"""
961+
Given input (the indices to forward), return the segmentation for each pass
962+
in the format of (input[start_idx:end_idx], output[start_idx:end_idx], start_idx).
963+
964+
Caller should guarantee input and output are having the size on dimension 0
965+
The returned segments are guaranteed to completely and non-overlappingly cover the input tensor.
966+
967+
In non-multipass-prefetch mode, it returns the input/output tensor itself.
968+
"""
969+
if multipass_prefetch_config is None:
970+
return [(input_tensor, output_tensor, 0)]
971+
mpp_config: MultiPassPrefetchConfig = multipass_prefetch_config
972+
973+
N = input_tensor.size(0)
974+
if N <= mpp_config.num_passes or mpp_config.num_passes == 1:
975+
# One row per pass, just don't split
976+
return [(input_tensor, output_tensor, 0)]
977+
978+
pass_size: int = max(
979+
(N + mpp_config.num_passes - 1) // mpp_config.num_passes,
980+
mpp_config.min_splitable_pass_size,
981+
)
982+
983+
return list(
984+
zip(
985+
torch.split(input_tensor, pass_size),
986+
torch.split(output_tensor, pass_size),
987+
range(0, N, pass_size),
988+
)
989+
)
990+
931991
def get_states(self, prefix: str) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
932992
if not hasattr(self, f"{prefix}_physical_placements"):
933993
raise DoesNotHavePrefix()
@@ -1197,7 +1257,12 @@ def forward( # noqa: C901
11971257
self._report_tbe_mem_usage()
11981258

11991259
if len(self.timesteps_prefetched) == 0:
1200-
self._prefetch(indices, offsets, vbe_metadata)
1260+
# In forward, we don't enable multi-pass prefetch as we want the process
1261+
# to be as fast as possible and memory usage doesn't matter (will be recycled
1262+
# by dense fwd/bwd)
1263+
self._prefetch(
1264+
indices, offsets, vbe_metadata, multipass_prefetch_config=None
1265+
)
12011266

12021267
if len(self.timesteps_prefetched) > 0:
12031268
self.timesteps_prefetched.pop(0)
@@ -1512,7 +1577,12 @@ def prefetch(
15121577
offsets,
15131578
batch_size_per_feature_per_rank,
15141579
)
1515-
self._prefetch(indices, offsets, vbe_metadata)
1580+
self._prefetch(
1581+
indices,
1582+
offsets,
1583+
vbe_metadata,
1584+
multipass_prefetch_config=self.multipass_prefetch_config,
1585+
)
15161586
if forward_stream is not None:
15171587
self._prefetch_tensors_record_stream(forward_stream)
15181588

@@ -1521,6 +1591,7 @@ def _prefetch(
15211591
indices: Tensor,
15221592
offsets: Tensor,
15231593
vbe_metadata: Optional[invokers.lookup_args.VBEMetadata] = None,
1594+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = None,
15241595
) -> None:
15251596
if not is_torchdynamo_compiling():
15261597
# Mutations of nn.Module attr forces dynamo restart of Analysis which increases compilation time
@@ -1537,81 +1608,90 @@ def _prefetch(
15371608
self.local_uvm_cache_stats.zero_()
15381609
self._report_io_size_count("prefetch_input", indices)
15391610

1540-
linear_cache_indices = torch.ops.fbgemm.linearize_cache_indices(
1541-
self.cache_hash_size_cumsum,
1542-
indices,
1543-
offsets,
1544-
vbe_metadata.B_offsets if vbe_metadata is not None else None,
1545-
vbe_metadata.max_B if vbe_metadata is not None else -1,
1546-
)
1547-
1548-
if (
1549-
self.record_cache_metrics.record_cache_miss_counter
1550-
or self.record_cache_metrics.record_tablewise_cache_miss
1611+
final_lxu_cache_locations = torch.empty_like(indices, dtype=torch.int32)
1612+
for (
1613+
partial_indices,
1614+
partial_lxu_cache_locations,
1615+
base_offset,
1616+
) in self.get_prefetch_passes(
1617+
multipass_prefetch_config, indices, final_lxu_cache_locations
15511618
):
1552-
lxu_cache_locations = torch.ops.fbgemm.lxu_cache_lookup(
1553-
linear_cache_indices,
1554-
self.lxu_cache_state,
1555-
self.total_cache_hash_size,
1556-
self.gather_uvm_cache_stats,
1557-
self.local_uvm_cache_stats,
1619+
linear_cache_indices = torch.ops.fbgemm.linearize_cache_indices(
1620+
self.cache_hash_size_cumsum,
1621+
partial_indices,
1622+
offsets,
1623+
vbe_metadata.B_offsets if vbe_metadata is not None else None,
1624+
vbe_metadata.max_B if vbe_metadata is not None else -1,
1625+
base_offset,
15581626
)
1559-
if self.record_cache_metrics.record_cache_miss_counter:
1560-
self._update_cache_miss_counter(
1561-
lxu_cache_locations, linear_cache_indices
1627+
1628+
if (
1629+
self.record_cache_metrics.record_cache_miss_counter
1630+
or self.record_cache_metrics.record_tablewise_cache_miss
1631+
):
1632+
lxu_cache_locations = torch.ops.fbgemm.lxu_cache_lookup(
1633+
linear_cache_indices,
1634+
self.lxu_cache_state,
1635+
self.total_cache_hash_size,
1636+
self.gather_uvm_cache_stats,
1637+
self.local_uvm_cache_stats,
1638+
)
1639+
if self.record_cache_metrics.record_cache_miss_counter:
1640+
self._update_cache_miss_counter(
1641+
lxu_cache_locations, linear_cache_indices
1642+
)
1643+
if self.record_cache_metrics.record_tablewise_cache_miss:
1644+
self._update_tablewise_cache_miss(
1645+
lxu_cache_locations, linear_cache_indices, offsets
1646+
)
1647+
1648+
if self.cache_algorithm == CacheAlgorithm.LRU:
1649+
torch.ops.fbgemm.lru_cache_populate(
1650+
self.weights_uvm,
1651+
self.cache_hash_size_cumsum,
1652+
self.total_cache_hash_size,
1653+
self.cache_index_table_map,
1654+
self.weights_offsets,
1655+
self.D_offsets,
1656+
linear_cache_indices,
1657+
self.lxu_cache_state,
1658+
self.lxu_cache_weights,
1659+
self.timestep,
1660+
self.lxu_state,
1661+
self.stochastic_rounding,
1662+
self.gather_uvm_cache_stats,
1663+
self.local_uvm_cache_stats,
1664+
self.lock_cache_line,
1665+
self.lxu_cache_locking_counter,
15621666
)
1563-
if self.record_cache_metrics.record_tablewise_cache_miss:
1564-
self._update_tablewise_cache_miss(
1565-
lxu_cache_locations, linear_cache_indices, offsets
1667+
elif self.cache_algorithm == CacheAlgorithm.LFU:
1668+
torch.ops.fbgemm.lfu_cache_populate(
1669+
self.weights_uvm,
1670+
self.cache_hash_size_cumsum,
1671+
self.total_cache_hash_size,
1672+
self.cache_index_table_map,
1673+
self.weights_offsets,
1674+
self.D_offsets,
1675+
linear_cache_indices,
1676+
self.lxu_cache_state,
1677+
self.lxu_cache_weights,
1678+
self.lxu_state,
1679+
self.stochastic_rounding,
15661680
)
15671681

1568-
if self.cache_algorithm == CacheAlgorithm.LRU:
1569-
torch.ops.fbgemm.lru_cache_populate(
1570-
self.weights_uvm,
1571-
self.cache_hash_size_cumsum,
1572-
self.total_cache_hash_size,
1573-
self.cache_index_table_map,
1574-
self.weights_offsets,
1575-
self.D_offsets,
1682+
torch.ops.fbgemm.lxu_cache_lookup(
15761683
linear_cache_indices,
15771684
self.lxu_cache_state,
1578-
self.lxu_cache_weights,
1579-
self.timestep,
1580-
self.lxu_state,
1581-
self.stochastic_rounding,
1685+
self.total_cache_hash_size,
15821686
self.gather_uvm_cache_stats,
15831687
self.local_uvm_cache_stats,
1584-
self.lock_cache_line,
1585-
self.lxu_cache_locking_counter,
1586-
)
1587-
elif self.cache_algorithm == CacheAlgorithm.LFU:
1588-
torch.ops.fbgemm.lfu_cache_populate(
1589-
self.weights_uvm,
1590-
self.cache_hash_size_cumsum,
1591-
self.total_cache_hash_size,
1592-
self.cache_index_table_map,
1593-
self.weights_offsets,
1594-
self.D_offsets,
1595-
linear_cache_indices,
1596-
self.lxu_cache_state,
1597-
self.lxu_cache_weights,
1598-
self.lxu_state,
1599-
self.stochastic_rounding,
1688+
lxu_cache_locations_output=partial_lxu_cache_locations,
16001689
)
16011690

16021691
assert (
16031692
len(self.lxu_cache_locations_list) < self.max_prefetch_depth
16041693
), 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()"
1605-
1606-
lxu_cache_locations = torch.ops.fbgemm.lxu_cache_lookup(
1607-
linear_cache_indices,
1608-
self.lxu_cache_state,
1609-
self.total_cache_hash_size,
1610-
self.gather_uvm_cache_stats,
1611-
self.local_uvm_cache_stats,
1612-
)
1613-
1614-
self.lxu_cache_locations_list.append(lxu_cache_locations)
1694+
self.lxu_cache_locations_list.append(final_lxu_cache_locations)
16151695

16161696
if self.gather_uvm_cache_stats:
16171697
# 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
@@ -26,6 +26,7 @@
2626
)
2727
from fbgemm_gpu.split_table_batched_embeddings_ops_training import (
2828
ComputeDevice,
29+
MultiPassPrefetchConfig,
2930
SplitTableBatchedEmbeddingBagsCodegen,
3031
)
3132

@@ -98,6 +99,7 @@ def generate_cache_tbes(
9899
stochastic_rounding: bool = False,
99100
gather_uvm_cache_stats: bool = False,
100101
reporter_config: Optional[TestingStatsReporterConfig] = None,
102+
multipass_prefetch_config: Optional[MultiPassPrefetchConfig] = None,
101103
) -> Tuple[
102104
SplitTableBatchedEmbeddingBagsCodegen,
103105
SplitTableBatchedEmbeddingBagsCodegen,
@@ -155,6 +157,7 @@ def generate_cache_tbes(
155157
cache_precision=weights_cache_precision,
156158
gather_uvm_cache_stats=gather_uvm_cache_stats,
157159
stats_reporter_config=reporter_config,
160+
multipass_prefetch_config=multipass_prefetch_config,
158161
)
159162

160163
if use_int_weight:

0 commit comments

Comments
 (0)