Skip to content

Commit 44ecc17

Browse files
kausvfacebook-github-bot
authored andcommitted
MLPerf DLRM-v2 train-perf benchmark on torch_tpu (meta-pytorch#4587)
Summary: Adds `train_perf_mlperf_tpu.py`, a steady-state training-throughput benchmark for the faithful MLPerf DLRM-v2 model on the torch_tpu stack (torch side only, no JAX). Same model as the GPU `mlperf_dlrm` runner in D110238830: torchrec `DLRM_DCN` (DCNv2, 3 layers / low-rank 512) over a multi-hot `EmbeddingBagCollection` (pool sum 214), `embedding_dim` 128, dense arch `[512,256,128]`, over arch `[1024,1024,512,256,1]`, per-element Adagrad. Cardinalities selectable via `--cardinality` (`shrunk` sum 30M / `canonical` sum 228M). Embeddings are row-wise sharded on the `UNFUSED_TPU` compute kernel under `DistributedModelParallel` over `tpu_dist`; the dense DCN/MLP path runs on TPU via torch_tpu. Reports steady-state ms/step and K samples/s/chip, the same metric as the jte TPU side, so it is a direct third comparison point for the GPU vs TPU per-chip gap-closure work (B200 GPU vs v7x jte-JAX vs v7x torch_tpu). Faithful-model caveat: the full MLPerf model uses pooled, multi-hot embeddings, which produce an uneven row-wise all2all. The torch_tpu RW path was first brought up for `EmbeddingCollection` / 1-hot / even-split only, so running this faithful config may require pooled-multi-hot / uneven-all2all support that is still landing. Differential Revision: D110794150
1 parent c246740 commit 44ecc17

3 files changed

Lines changed: 159 additions & 0 deletions

File tree

torchrec/distributed/batched_embedding_kernel.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4795,3 +4795,136 @@ def forward(self, features: KeyedJaggedTensor) -> torch.Tensor:
47954795
self._emb_modules[self._feature_table_map[feature_idx]](feature_values)
47964796
)
47974797
return outputs[0] if len(outputs) == 1 else torch.cat(outputs, dim=0)
4798+
4799+
4800+
class BatchedTPUEmbeddingBag(BaseBatchedEmbeddingBag[torch.Tensor]):
4801+
"""Pooled TPU compute kernel (`UNFUSED_TPU`).
4802+
4803+
Pooled sibling of `BatchedTPUEmbedding`, and a drop-in alongside
4804+
`BatchedDenseEmbeddingBag` in the pooled dispatch
4805+
(`GroupedPooledEmbeddingsLookup._create_embedding_kernel`). Backs the lookup with one
4806+
`TPUEmbeddingUnfused` table per table in the group; `forward` routes each feature to
4807+
its table, gathers its ids, and pools per sample (SUM or MEAN per `config.pooling`).
4808+
4809+
Pooling is done in torch (scatter-add over per-id sample indices) on top of the
4810+
unfused gather, so no dedicated pooled kernel is required and the backward flows
4811+
through the gather op's autograd.
4812+
4813+
Args:
4814+
config (GroupedEmbeddingConfig): grouped table config (one or more tables).
4815+
pg (Optional[dist.ProcessGroup]): process group (unused locally).
4816+
device (Optional[torch.device]): compute device (e.g. ``"tpu"``).
4817+
sharding_type (Optional[ShardingType]): sharding type (for the pooling mode).
4818+
4819+
Example::
4820+
4821+
kernel = BatchedTPUEmbeddingBag(grouped_config, device="tpu")
4822+
out = kernel(features) # [batch, sum(embedding_dim over features)]
4823+
"""
4824+
4825+
def __init__(
4826+
self,
4827+
config: GroupedEmbeddingConfig,
4828+
pg: Optional[dist.ProcessGroup] = None,
4829+
device: Optional[torch.device] = None,
4830+
sharding_type: Optional[ShardingType] = None,
4831+
) -> None:
4832+
super().__init__(config, pg, device, sharding_type)
4833+
# Lazy import to avoid pulling in experimental TPU code at module load time.
4834+
from torchrec.experimental.torch_tpu.modules.embedding_modules import (
4835+
TPUEmbeddingUnfused,
4836+
)
4837+
4838+
dtype = data_type_to_sparse_type(config.data_type).as_dtype()
4839+
# One TPUEmbeddingUnfused per table in the group (mirrors BatchedTPUEmbedding);
4840+
# the base class fills _local_rows / _local_cols / _feature_table_map.
4841+
self._emb_modules: nn.ModuleList = nn.ModuleList()
4842+
for local_rows, local_cols in zip(self._local_rows, self._local_cols):
4843+
self._emb_modules.append(
4844+
TPUEmbeddingUnfused(
4845+
num_embeddings=local_rows,
4846+
embedding_dim=local_cols,
4847+
device=device,
4848+
dtype=dtype,
4849+
)
4850+
)
4851+
self.init_parameters()
4852+
4853+
@property
4854+
# pyrefly: ignore [bad-override] # TPUEmbeddingUnfused is not one of the
4855+
# fbgemm codegen types the base class enumerates (same as the Triton kernel).
4856+
def emb_module(self) -> "TPUEmbeddingUnfused": # noqa: F821
4857+
# pyre-ignore[16]: ModuleList returns Module, pyre thinks Union
4858+
return self._emb_modules[0]
4859+
4860+
def split_embedding_weights(self) -> List[torch.Tensor]:
4861+
# pyre-ignore[16]
4862+
return [emb_module.weight for emb_module in self._emb_modules]
4863+
4864+
def named_split_embedding_weights(
4865+
self, prefix: str = "", recurse: bool = True, remove_duplicate: bool = True
4866+
) -> Iterator[Tuple[str, torch.Tensor]]:
4867+
assert (
4868+
remove_duplicate
4869+
), "remove_duplicate=False not supported in named_split_embedding_weights"
4870+
for table, emb_module in zip(self._config.embedding_tables, self._emb_modules):
4871+
# pyre-ignore[16]
4872+
yield append_prefix(prefix, f"{table.name}.weight"), emb_module.weight
4873+
4874+
def named_parameters(
4875+
self, prefix: str = "", recurse: bool = True, remove_duplicate: bool = True
4876+
) -> Iterator[Tuple[str, nn.Parameter]]:
4877+
for table, emb_module in zip(self._config.embedding_tables, self._emb_modules):
4878+
# pyre-ignore[7]
4879+
yield append_prefix(prefix, f"{table.name}.weight"), emb_module.weight
4880+
4881+
def _pool(
4882+
self, gathered: torch.Tensor, lengths: torch.Tensor, batch_size: int
4883+
) -> torch.Tensor:
4884+
"""Pool a feature's per-id embeddings [L, dim] into [batch, dim].
4885+
4886+
Sum-pool by scatter-adding each id's row into its sample slot; MEAN divides by
4887+
the bag length. `lengths` are this feature's per-sample bag sizes (may be jagged
4888+
after the row-wise all2all), summing to L. Uses only arange / repeat_interleave /
4889+
index_add_ / div, so it lowers on TPU without a bespoke pooled kernel.
4890+
"""
4891+
dim = gathered.shape[1]
4892+
device = gathered.device
4893+
pooled = torch.zeros(batch_size, dim, dtype=gathered.dtype, device=device)
4894+
# sample index for each gathered row: sample b repeated lengths[b] times.
4895+
sample_idx = torch.repeat_interleave(
4896+
torch.arange(batch_size, device=device), lengths
4897+
)
4898+
pooled.index_add_(0, sample_idx, gathered)
4899+
if self._pooling == PoolingMode.MEAN:
4900+
denom = lengths.clamp(min=1).unsqueeze(1).to(pooled.dtype)
4901+
pooled = pooled / denom
4902+
return pooled
4903+
4904+
# pyrefly: ignore [bad-override] # same signature shape as the sequence kernel
4905+
def forward(self, features: KeyedJaggedTensor) -> torch.Tensor:
4906+
batch_size = features.stride()
4907+
length_per_key = features.length_per_key()
4908+
# KJT lengths are feature-major: feature i's per-sample bag sizes are the
4909+
# i-th block of batch_size entries.
4910+
all_lengths = features.lengths()
4911+
per_feature_values = (
4912+
torch.split(features.values(), length_per_key)
4913+
if len(length_per_key) > 1
4914+
else [features.values()]
4915+
)
4916+
outputs: List[torch.Tensor] = []
4917+
for feature_idx, feature_values in enumerate(per_feature_values):
4918+
emb_module = self._emb_modules[self._feature_table_map[feature_idx]]
4919+
# pyre-ignore[9, 16]: ModuleList returns Module, pyre thinks Union;
4920+
# the table's `weight` is a Tensor so `.device` is a torch.device.
4921+
device: torch.device = emb_module.weight.device
4922+
gathered = emb_module(
4923+
feature_values.to(device=device, dtype=torch.int32)
4924+
) # [L_i, dim]
4925+
lengths_i = all_lengths[
4926+
feature_idx * batch_size : (feature_idx + 1) * batch_size
4927+
].to(device)
4928+
outputs.append(self._pool(gathered, lengths_i, batch_size))
4929+
# Pooled features concatenate along the embedding dim -> [batch, sum(dim)].
4930+
return outputs[0] if len(outputs) == 1 else torch.cat(outputs, dim=1)

torchrec/distributed/embedding_lookup.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
BatchedFusedEmbedding,
5050
BatchedFusedEmbeddingBag,
5151
BatchedTPUEmbedding,
52+
BatchedTPUEmbeddingBag,
5253
KeyValueEmbedding,
5354
KeyValueEmbeddingBag,
5455
ShardedBatchedFusedEmbedding,
@@ -731,6 +732,13 @@ def _create_embedding_kernel(
731732
pg=pg,
732733
device=device,
733734
)
735+
elif config.compute_kernel == EmbeddingComputeKernel.UNFUSED_TPU:
736+
return BatchedTPUEmbeddingBag(
737+
config=config,
738+
pg=pg,
739+
device=device,
740+
sharding_type=sharding_type,
741+
)
734742
elif config.compute_kernel in {
735743
EmbeddingComputeKernel.KEY_VALUE,
736744
}:

torchrec/distributed/embeddingbag.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,24 @@ def _initialize_torch_state(self, skip_registering: bool = False) -> None: # no
13811381
self._table_name_to_config[table_name].data_type
13821382
)
13831383
),
1384+
# Set grad-ness so ShardedTensor init does not reject an
1385+
# unfused (autograd-trained) kernel whose shard is a
1386+
# grad-requiring nn.Parameter. Derive it from the compute
1387+
# kernel rather than the local shard: the kernel comes from
1388+
# the sharding plan and is identical on every rank, whereas
1389+
# probing local_shards would record False on ranks that hold
1390+
# no shard (e.g. table-wise), yielding globally inconsistent
1391+
# TensorProperties for the same logical tensor. Only the
1392+
# unfused kernels (DENSE, UNFUSED_TPU) keep grad-requiring
1393+
# weights; fused/KV/quant kernels train via in-backward
1394+
# optimizers on non-grad buffers.
1395+
requires_grad=(
1396+
_model_parallel_name_to_compute_kernel[table_name]
1397+
in {
1398+
EmbeddingComputeKernel.DENSE.value,
1399+
EmbeddingComputeKernel.UNFUSED_TPU.value,
1400+
}
1401+
),
13841402
)
13851403

13861404
sharded_tensor_metadata = sharding_spec.build_metadata(

0 commit comments

Comments
 (0)