Skip to content

[feat] support probabilistic admission strategy for dynamicemb - #680

Merged
tiankongdeguiji merged 4 commits into
alibaba:masterfrom
tiankongdeguiji:feat/dynamicemb-probabilistic-admission
Sep 21, 2026
Merged

tiankongdeguiji merged 4 commits into
alibaba:masterfrom
tiankongdeguiji:feat/dynamicemb-probabilistic-admission

Conversation

@tiankongdeguiji

Copy link
Copy Markdown
Collaborator

What

dynamicemb gained a second admission strategy, and this exposes it on dynamicemb { }:

dynamicemb {
    max_capacity: 100000
    probabilistic_admission_strategy {
        probability: 0.1
    }
}

Admission is consulted only for a key that is missing, so a key gets a fresh coin toss every
time it turns up and is not yet in the table: it takes 1 / probability appearances on average
to be admitted. That filters by frequency the way frequency_admission_strategy does, but
without counting anything — no KVCounter, so no second hash table in HBM and no counter column
in the checkpoint. The cheap option when a table only needs long-tail ids kept out.

frequency_admission_strategy is unchanged for users, but its plumbing moved: upstream
(NVIDIA/recsys-examples#488) put the counter on the strategy that counts with it and deprecated
DynamicEmbTableOptions.admission_counter, so the KVCounter is now passed to
FrequencyAdmissionStrategy(threshold=..., counter=...) and the shard storage estimator reads it
from there. A probabilistic table contributes no counter bytes to the estimate.

One behavior change falls out of the same upstream refactor: admission strategies are now
compared by value rather than by object identity, so two dynamicemb tables configured alike fuse
into one storage where they previously could not (each table still keeps its own
counter_capacity).

This needs a dynamicemb built from recsys-examples 9643985, hence the pins in
requirements/cu1{26,29,30}.txt and the install line in the doc.

Test Plan

Everything below ran against a dynamicemb wheel built locally from 9643985
(0.1.0+20260920.9643985.cu130, cp311) on 4x A10.

  • tzrec/utils/dynamicemb_util_test.py — 26/26, including 8 new AdmissionStrategyTest cases:
    both oneof arms translate, the counter lands on the frequency strategy at the per-rank capacity,
    admission_counter stays unset and no DeprecationWarning is raised, the non-admitted
    initializer defaults to CONSTANT 0.0 and honors an explicit one, identically configured tables
    produce equal grouping keys, and the shard storage estimate grows by exactly the counter's size
    for frequency admission and not at all for probabilistic.
  • tzrec/tests/rank_integration_test.py::test_multi_tower_din_with_dynamicemb_train_eval — the
    mock config's second dynamicemb feature (id_8) now carries
    probabilistic_admission_strategy, so the 2-rank train_eval + eval covers one frequency-admitted
    and one probabilistic table in the same model, including dump/load.
  • Upstream's own admission suite on that wheel — 12/12, including the statistical checks on the
    admission rate a given probability produces.
  • python tzrec/tests/run.py --scope gpu — full lane, as the dependency-upgrade check, since the
    wheel also picks up recsys-examples#487 (FTRL optimizer).
  • pre-commit run on the changed files and pyrefly check — clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_019t8YACeFxVxCDHMXB9a2j2

dynamicemb now admits a missing key by a coin toss as well as by counting it:
a key gets a fresh draw every time it turns up and is not yet in the table, so
it takes 1 / probability appearances on average to get in. That filters by
frequency without a counter, so the table costs no extra HBM and writes no
counter into the checkpoint.

The same upstream change moves the frequency counter onto the strategy that
counts with it and deprecates DynamicEmbTableOptions.admission_counter, so the
counter is passed to FrequencyAdmissionStrategy and the shard storage
estimator reads it from there. This requires the bumped dynamicemb wheel, hence
the requirements pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8YACeFxVxCDHMXB9a2j2
@tiankongdeguiji
tiankongdeguiji force-pushed the feat/dynamicemb-probabilistic-admission branch from 7ea81c5 to 904389e Compare September 20, 2026 11:20
@tiankongdeguiji tiankongdeguiji added the claude-review Let Claude Review label Sep 20, 2026
@github-actions github-actions Bot removed the claude-review Let Claude Review label Sep 20, 2026
Comment thread tzrec/utils/dynamicemb_util_test.py Outdated
admit_strategy, dynamicemb_util.ProbabilisticAdmissionStrategy
)
self.assertEqual(admit_strategy.probability, 0.25)
self.assertFalse(hasattr(admit_strategy, "counter"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, non-blocking: assertFalse(hasattr(admit_strategy, "counter")) pins upstream's attribute layout rather than tzrec behavior. FrequencyAdmissionStrategy.counter is already Optional, so a benign upstream refactor exposing a shared counter property on both strategies (None for probabilistic) would fail this test while tzrec stays correct. The guarantee that matters — probabilistic admission allocates no counter HBM — is already pinned behaviorally by test_counter_hbm_lands_only_for_frequency_admission. Suggest dropping this line; the isinstance + probability assertions above are sufficient.

initializer_args=non_admitted_initializer_args,
)
elif admission_strategy_type == "probabilistic_admission_strategy":
admit_strategy = ProbabilisticAdmissionStrategy(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, non-blocking: the [0, 1] range check for probability lives only in the pinned wheel (verified present at 9643985: ProbabilisticAdmissionStrategy.__init__ raises ValueError outside the range, and NaN fails the same check — good fail-fast behavior at plan time). But nothing in tzrec's own suite pins that contract: AdmissionStrategyTest only exercises 0.25/0.5, and the wheel is built from a moving upstream branch, so a future pin bump could silently lose the validation. A cheap boundary case would lock it in:

def test_out_of_range_probability_rejected(self):
    for bad in (-0.1, 1.5):
        with self.assertRaises(ValueError):
            self._options(
                probabilistic_admission_strategy=(
                    feature_pb2.DynamicEmbProbabilisticAdmissionStrategy(
                        probability=bad
                    )
                )
            )

(optionally also asserting 0.0/1.0 construct successfully).

pip install dynamicemb==0.1.0+20260920.9643985.${DEVICE} -f https://tzrec.oss-accelerate.aliyuncs.com/third_party/dynamicemb/${DEVICE}/repo.html
```

注:同一个 FeatureGroup 中若存在多个配置了 DynamicEmbedding 的特征,底层 dynamicemb 会自动将这些表融合到同一份存储里(table fusion),共享 cache/admission counter,降低显存占用并减少内存碎片,无需额外配置。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional wording nit: this note predates the counter-less strategy — probabilistic tables have no admission counter (as the new bullet below says), so fused probabilistic tables share only the cache. Something like "共享 cache(配置 frequency 准入时还包含 admission counter,probabilistic 准入无 counter)" would keep it accurate. For clarity: mixing the two strategies in one FeatureGroup remains fine — different grouping keys just means those tables don't fuse (this PR's integration config relies on that), no error.

@github-actions

Copy link
Copy Markdown
Contributor

Code review — 5-area static review + upstream verification

Overall LGTM: clean, well-scoped change with strong test coverage. Three minor non-blocking inline comments; nothing blocking.

Independently verified (against NVIDIA/recsys-examples @ 9643985, the exact pinned build):

  • Strategy constructor signatures match the new call sites; DynamicEmbTableOptions.admission_counter is deprecated upstream (warns at init), and no stale references remain anywhere in tzrec.
  • ProbabilisticAdmissionStrategy.__init__ rejects probability outside [0, 1] (NaN included) with a ValueError at plan-construction time — fail-fast, consistent across ranks.
  • The estimator change is a genuine correctness fix: under the new pin the old admission_counter is not None check would never fire again, silently dropping per-rank counter bytes from the HBM estimate (planner OOM risk). Counter accounting also stays correct under the new by-value fusion — upstream's MultiTableKVCounter keeps one capacity per logical table, matching the per-table estimator term, and KVCounter's grouping key deliberately excludes capacity.
  • All 9 pinned wheels (cu126/cu129/cu130 × cp310/311/312) are published on the OSS index; the doc install line matches the pins; no leftover references to the old 5948173 build.
  • Doc claims verified: the Counter table is none warning is real (upstream dump()/load()), the CONSTANT 0.0 non-admitted initializer default matches _build_dynamicemb_initializer(..., is_eval=True), and counter_capacity defaults to max_capacity.
  • Integration config: both dynamicemb features sit in the same group (deep) with different strategies — confirmed against upstream grouping that this is safe (differing grouped keys simply don't fuse), so the test exercises frequency and probabilistic admission in one model.

Minor comments (all optional):

  1. dynamicemb_util_test.py:259 — assertFalse(hasattr(..., "counter")) couples the test to upstream attribute layout; the guarantee is already pinned behaviorally by the shard-storage test.
  2. dynamicemb_util.py:388 — no tzrec-side test pins the wheel's out-of-range probability rejection; suggested a cheap assertRaises(ValueError) boundary case.
  3. docs/source/feature/dynamicemb.md:12 — the fusion note's "共享 cache/admission counter" predates the counter-less strategy.

Reviewed areas: code quality, performance, test coverage, documentation accuracy, security (supply chain, input validation, multi-process safety). All five reviews completed.

hasattr(strategy, "counter") pins upstream's attribute layout, which tzrec
does not depend on -- the estimator branches on the strategy type -- and the
guarantee it stood for, that a probabilistic table adds no counter bytes to
the HBM estimate, is already pinned by the shard storage test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8YACeFxVxCDHMXB9a2j2
@tiankongdeguiji

Copy link
Copy Markdown
Collaborator Author

Thanks — went through all three.

Applied (1/3): dropped assertFalse(hasattr(admit_strategy, "counter")) and renamed the
test to test_probabilistic_strategy_carries_its_probability. It was the only assertion in the
new tests pinning upstream's attribute layout rather than the translation result, nothing in
tzrec depends on the attribute's absence (the estimator branches on the strategy type), and the
guarantee it stood for is already pinned by test_counter_hbm_lands_only_for_frequency_admission.

Not applying (2/3): the out-of-range probability test. The premise is right — without the
guard a typo is silent rather than loud (driving MultiTableProbabilisticAdmitter directly,
p=1.5 admits every key and p=-0.1 admits none, no error) — but the test belongs where it
already is: recsys-examples@9643985 has
test/unit_tests/admission/test_probabilistic_admission.py::test_rejects_a_probability_outside_the_unit_interval,
asserting ValueError for exactly 1.5 and -0.1, and it runs in upstream CI and in the 12/12
admission suite this PR ran against the pinned wheel. Adding it here would also be the only one
of tzrec's 260 assertRaises sites that pins a dependency's input validation instead of a tzrec
function's error — the same coupling comment 1 objects to.

Not applying (3/3): the fusion note wording. The observation is correct, but that line is
deliberately left byte-identical to master.

@tiankongdeguiji
tiankongdeguiji merged commit ae24350 into alibaba:master Sep 21, 2026
7 checks passed
@tiankongdeguiji
tiankongdeguiji deleted the feat/dynamicemb-probabilistic-admission branch September 21, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants