Skip to content

Commit 6d8430b

Browse files
kausvmeta-codesync[bot]
authored andcommitted
MLPerf DLRM-v2 train-perf benchmark on torch_tpu (#4587)
Summary: Pull Request resolved: #4587 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. Reviewed By: Ali-Tehrani, TroyGarden Differential Revision: D110794150 fbshipit-source-id: bc1cbd166f954841117af204887b0fc3b02e7acf
1 parent d9e82ed commit 6d8430b

5 files changed

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

13861395
sharded_tensor_metadata = sharding_spec.build_metadata(

torchrec/experimental/torch_tpu/benchmarks/benchmark_single_lookup.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from typing import List
1818

1919
import torch
20+
21+
# pyre-ignore[21]: torch_tpu ships in the TPU pod venv, not as a buck dep.
2022
from torch_tpu._internal import profiler, sync
2123
from torchrec.distributed.batched_embedding_kernel import BatchedTPUEmbedding
2224
from torchrec.distributed.embedding_types import (
@@ -135,6 +137,7 @@ def main() -> None:
135137
# Copy weights from reference into the TPU tables so outputs are comparable.
136138
# split_embedding_weights() returns one weight per table, in table order.
137139
for weight, config in zip(tpu_kernel.split_embedding_weights(), embedding_configs):
140+
# pyre-ignore[6]: EmbeddingCollection.embeddings is a ModuleDict, so indexing it types as Module.
138141
weight.data.copy_(ref_module.embeddings[config.name].weight.data)
139142

140143
# --- Accuracy test: compare outputs across different batch sizes ---
@@ -153,6 +156,7 @@ def main() -> None:
153156
)
154157
# CPU embedding needs int64 on torch 2.13
155158
kjt = model_input.idlist_features
159+
assert isinstance(kjt, KeyedJaggedTensor)
156160
# Convert to int32/TPU outside the forward, before ref_module caches a CPU _jt_dict on kjt.
157161
kjt_tpu = KeyedJaggedTensor(
158162
keys=kjt.keys(),
@@ -180,6 +184,7 @@ def main() -> None:
180184
print("\nTRACE_DIR unset, skipping the profiled step")
181185
else:
182186
print(f"Profiler Start, traces printed to {traces_dir}")
187+
tpu_output: torch.Tensor | None = None
183188
with profiler.profile(
184189
activities=[
185190
profiler.ProfilerActivity.CPU,
@@ -199,6 +204,7 @@ def main() -> None:
199204
)
200205
# CPU embedding needs int64
201206
kjt = model_input.idlist_features
207+
assert isinstance(kjt, KeyedJaggedTensor)
202208

203209
# Convert to int32/TPU outside the forward.
204210
kjt_tpu = KeyedJaggedTensor(
@@ -208,6 +214,9 @@ def main() -> None:
208214
)
209215
tpu_output = tpu_kernel(kjt_tpu)
210216

217+
assert (
218+
tpu_output is not None
219+
), "profiled step never ran; PROFILE_NUMBER_TIMES must be > 0"
211220
print("Profiled TPU output shape:", tuple(tpu_output.shape))
212221

213222
# --- Benchmark here -------
@@ -243,6 +252,7 @@ def main() -> None:
243252
)
244253
# CPU embedding needs int64
245254
kjt = model_input.idlist_features
255+
assert isinstance(kjt, KeyedJaggedTensor)
246256
# Convert to int32/TPU
247257
kjt_tpu = KeyedJaggedTensor(
248258
keys=kjt.keys(),
@@ -267,6 +277,7 @@ def main() -> None:
267277
)
268278
# CPU embedding needs int64
269279
kjt = model_input.idlist_features
280+
assert isinstance(kjt, KeyedJaggedTensor)
270281
# Convert to int32/TPU
271282
kjt_tpu = KeyedJaggedTensor(
272283
keys=kjt.keys(),

0 commit comments

Comments
 (0)