[feat] support initial_accumulator_value on dynamicemb tables - #669
tiankongdeguiji merged 3 commits into
Conversation
initial_accumulator_value only reached FBGEMM TBE tables, via an env var read by the apply_split_helper patch, because SplitTableBatchedEmbeddingBagsCodegen rejects it as a kwarg and create_sparse_optimizer therefore popped it out of the fused params. dynamicemb tables read the same fused params and kept the 0.0 default, so two tables under one config trained with different Adagrad semantics. Carry the value to dynamicemb tables through get_additional_fused_params of the customized-kernel parameter sharding, which torchrec merges per table after the optimizer kwargs and dynamicemb forwards to its table module. Also add the field to FusedRowWiseAdagradOptimizer, which both backends support. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vLyBqTcb9nAcDbXN37nUs
The value comes from pipeline.config, not from the environment, and every rank sets it in its own process before building any table, so os.environ bought nothing while costing a str round-trip, inheritance by child processes and an FBGEMM_-prefixed name that no fbgemm code reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vLyBqTcb9nAcDbXN37nUs
| # Adagrad of tensorflow has param initial_accumulator_value with default value 0.1 | ||
| momentum1_init_value_str = os.environ.get("FBGEMM_MOMENTUM1_STATE_INIT_VALUE", None) | ||
| init_value = 0.0 | ||
| init_value = sparse_init_accumulator_value() |
There was a problem hiding this comment.
The rewritten gate (env var → module global) and the momentum1 fill below it have no direct test coverage: the new optimizer_builder_test case asserts only the kwargs/global plumbing, and the e2e multi_tower_din_fg_mock_adagrad_init_acc.config run asserts success only — a regression in use_init_value or in the torch.full fills would pass CI. Since this PR reworks exactly this mechanism (and extends it to rowwise adagrad), consider a small unit test that invokes apply_split_helper with a fake split / set_attr_fn / persistent_state_fn and asserts the buffer contents for prefix="momentum1".
| customized_compute_kernel=DynamicEmbKernel, | ||
| dist_type="roundrobin", | ||
| dynamicemb_options=dynamicemb_options, | ||
| initial_accumulator_value=sparse_init_accumulator_value(), |
There was a problem hiding this comment.
This reads the process global at plan time, so correctness depends on create_sparse_optimizer running before any plan generation. _train_and_evaluate satisfies that (main.py:885 before :906), but evaluate / export / predict build plans with the 0.0 default — benign today (state is checkpoint-restored and no updates run), yet the contract is implicit and untested. Consider documenting it in the set_sparse_init_accumulator_value docstring ("must be called before planning / TBE init"), mirroring the set_auto_retain_evicted_keys pattern in this module.
|
|
||
| **Note**: 被分片为`data_parallel`的Embedding表不受sparse_optimizer管理,实际由dense_optimizer更新,并跟随dense_optimizer的LR策略,详见[训练文档](../usage/train.md)的Embedding分片约束章节 | ||
|
|
||
| **Note**: `adagrad_optimizer`和`rowwise_adagrad_optimizer`的`initial_accumulator_value`(对齐TensorFlow Adagrad的同名参数,默认0.0)对普通Embedding表和[dynamicemb](../feature/dynamicemb.md)表同时生效,新插入的key其accumulator会初始化为该值 |
There was a problem hiding this comment.
Two wording issues:
- “(对齐TensorFlow Adagrad的同名参数,默认0.0)” can be misread as TensorFlow's default being 0.0 — TF's is 0.1 (as the code comment in
tzrec/optim/optimizer.pynotes). Suggest e.g. “对齐TensorFlow Adagrad的同名参数(TF默认0.1),TZRec默认0.0”. - “新插入的key其accumulator会初始化为该值” describes dynamicemb (HKV) semantics only — for regular FBGEMM tables the whole
momentum1buffer is pre-filled at table construction, with no notion of newly inserted keys. Suggest scoping that clause to the dynamicemb表.
Code review — PR #669Reviewed with five parallel focus areas (code quality, performance, test coverage, documentation accuracy, multi-process/state safety), plus independent verification of the cross-library plumbing. No blocking issues found — the design is sound. Verified as correct:
Posted 3 inline comments:
Minor nits (no action required):
🤖 Generated with Claude Code |
The apply_split_helper patch had no direct assertion on the buffers it fills, so a regression in its gate or in the fills would have passed CI. Cover both adagrad variants with a CPU table, and record on the setter when the value is read, which the call site's comment was carrying instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013vLyBqTcb9nAcDbXN37nUs
|
Addressed the review in bb959d7. Fixed
Not changed
Re-ran after the change: the new CPU test, 🤖 Generated with Claude Code |
Background
initial_accumulator_value(TensorFlow Adagrad's same-named parameter, which EasyRec users rely on) was already exposed onFusedAdagradOptimizer, but it only reached FBGEMM TBE tables.SplitTableBatchedEmbeddingBagsCodegenhas no such kwarg and would raise on the**fused_paramssplat, socreate_sparse_optimizerpopped the key out of the optimizer kwargs and instead passed it through an env var consumed by theapply_split_helperpatch.dynamicemb tables read the very same fused params, so the key never reached them and
OptimizerArgs.initial_accumulator_valuestayed at its0.0default. A model with both kinds of tables therefore trained them with different Adagrad semantics, silently.FusedRowWiseAdagradOptimizerhad no such field at all, although both backends support one.Changes
get_additional_fused_params()of the customized-kernel parameter sharding. torchrec merges that per table after the optimizer kwargs, dynamicemb strips only its own planner keys, andBatchedDynamicEmbeddingTablestakesinitial_accumulator_valueas a plain kwarg — so no dynamicemb-side change and no new monkeypatch.initial_accumulator_valuetoFusedRowWiseAdagradOptimizer. dynamicemb'sEXACT_ROWWISE_ADAGRADhonours it, and the existing FBGEMM patch already keys on themomentum1state prefix, so rowwise adagrad gets it on both backends.create_sparse_optimizerso both Adagrad variants are handled in one place, and wrap the env var in named accessors instead of repeating the FBGEMM string at the new call site.Alternatives considered
Letting the key stay in the optimizer kwargs and stripping it inside the FBGEMM kernel would have needed a second monkeypatch covering every
**fused_paramssplat site (split TBE, SSD TBE, ...). Sharder-levelfused_paramswere not usable either:DynamicEmbeddingBagCollectionShardersubclassesEmbeddingBagCollectionSharderand shards the whole EBC, so the value would have leaked onto the FBGEMM tables in the same collection. The per-table customized-kernel hook is the only place the two backends are actually distinguishable.Test Plan
optimizer_builder_testcase: adagrad / rowwise adagrad / unset — the key never reaches the returned kwargs and the recorded value matches the config.plan_util_test.PlanUtilDynamicEmbE2ETestcase: the generated sharding plan's parameter sharding carries the value in its additional fused params, dynamicemb'spop_additional_fused_paramsleaves it in place, andBatchedDynamicEmbeddingTablesstill declares the kwarg (so an upstream rename fails loudly instead of silently dropping the value).DistributedModelParallelshard that the real dynamicemb table'sOptimizerArgs.initial_accumulator_valueis the configured0.1, for bothadagrad_optimizerandrowwise_adagrad_optimizer, and that a plain FBGEMM rowwise-adagrad table'smomentum1buffer is filled with0.1.multi_tower_din_fg_dynamicemb_mock.confignow setsinitial_accumulator_value: 0.1, sotest_multi_tower_din_with_dynamicemb_train_evalexercises the path end to end — passes.test_multi_tower_din_with_fg_adagrad_init_acc_train_eval_export(the FBGEMM regression) and all oftzrec/optim,tzrec/utils/plan_util_test,tzrec/utils/dynamicemb_util_test,tzrec/main_testpass.pre-commit runon the touched files andpyrefly checkare clean.🤖 Generated with Claude Code
https://claude.ai/code/session_013vLyBqTcb9nAcDbXN37nUs