A recommender ranker usually stores a set of embedding tables. Each table belongs to a feature family: users, candidate items, boards, topics, queries, actions, creators, or other categorical ids. One candidate row references a few rows from several of those tables, and the ranker uses the returned vectors plus dense numeric features to build the tensor consumed by its interaction layers.

The distributed-training bottleneck comes from that fan-out. A rank does not need every row from every table. It needs the rows touched by its local batch, and some of those rows may be owned by other ranks.

One Candidate Row Fans Out

A ranker row starts as structured product facts. The model cannot do dense math over raw ids such as user_42 or board_789; it first maps each id to an integer row address in the matching table.

Feature familyValue in one rowTable readReturned object
Useruser_42user_embedding_table[42]user vector
Candidate Pinpin_77pin_embedding_table[77]candidate vector
Board contextboard_789board_embedding_table[789]board vector
Topics[topic_12, topic_48]lookup_many(topic_embedding_table, [12, 48])topic vectors, often pooled
Recent actions[save, click, hide]action_embedding_table[...]action vectors, often pooled or sequenced
Numeric featuresage, quality score, freshnessno embedding tablenormalized numbers

The model then assembles those pieces into its local input:

candidate_row = {
    "user_id": 42,
    "pin_id": 77,
    "board_id": 789,
    "topic_ids": [12, 48],
    "recent_actions": ["save", "click", "hide"],
    "pin_age_days": 3.0,
    "quality_score": 0.82,
}
 
looked_up_vectors = {
    "user": user_embedding_table[42],
    "pin": pin_embedding_table[77],
    "board": board_embedding_table[789],
    "topics": pool(lookup_many(topic_embedding_table, [12, 48])),
    "actions": encode_sequence(lookup_many(action_embedding_table, ["save", "click", "hide"])),
}
 
dense_features = normalize([candidate_row["pin_age_days"], candidate_row["quality_score"]])
ranker_input = assemble(looked_up_vectors, dense_features)
scores = interaction_layers(ranker_input)

assemble might concatenate vectors, project them to a common dimension, add feature-family embeddings, pool repeated features, or feed sequences into attention. The exact architecture varies. The invariant is that the interaction layers cannot run until the required vectors for that candidate row have arrived.

Why There Are Multiple Tables

Separate tables keep unrelated id spaces separate. user_42, pin_42, and board_42 can all have numeric id 42, but they do not mean the same thing and should not share one learned row.

Feature families also have different shapes and operating constraints:

ReasonExample
Cardinality differsthere may be far more Pins than action types
Dimensions differPin ids may deserve wider vectors than country ids
Update frequency differshot Pins and users appear in batches more often than rare boards
Fallbacks differa new Pin can use content features; a new action type may use a default row
Aggregation differstopic ids may be pooled, while recent actions may keep sequence order

This is why a recommender can be embedding-heavy even when each example touches only a small number of rows. The full model stores rows for huge id spaces, while each forward pass reads a sparse subset.

Serving Versus Training

Serving and training both perform feature-family lookups, but they use different model state.

PathTable stateWhat happens
Servingpublished model versionread the rows needed for one request’s candidate rows
Trainingmutable checkpoint plus optimizer stateread touched rows, compute loss, send gradients back to touched rows

Serving wants stable low-latency reads. Training wants mutable rows and optimizer bookkeeping. A row that changes during training does not affect serving until a trained checkpoint is exported and rolled out.

Why Distributed Training Needs Row Movement

In distributed training, one rank can own the local batch examples while other ranks own some of the embedding rows referenced by those examples.

rank 0 local batch:
  row A: user_42, pin_77, board_789
  row B: user_99, pin_12, board_555
 
row ownership:
  user rows live on rank 0
  pin rows live on rank 5
  board rows live on rank 12

Rank 0 can read the user rows locally, but it cannot build the full ranker input until rank 5 returns the Pin vectors and rank 12 returns the board vectors:

rank 0 -> rank 5:  lookup pin_77, pin_12
rank 0 -> rank 12: lookup board_789, board_555
 
rank 5  -> rank 0: vector(pin_77), vector(pin_12)
rank 12 -> rank 0: vector(board_789), vector(board_555)

The forward pass waits for those returned vectors. The backward pass sends sparse gradients for pin_77, pin_12, board_789, and board_555 back to the ranks that own those rows. That is the lookup exchange explained in Distributed Training for Embedding-Heavy Recommenders.

Batch Path

The batch path is:

one candidate row
  -> many feature-family lookups
  -> one assembled ranker input tensor
  -> local interaction layers
  -> per-candidate scores

Distributed training keeps the same model math but changes where the rows live. The bottleneck appears when the rank with the examples must wait for vectors from remote row owners before it can do its local compute.

Sources

See also