[feat] SID: in-model candidate-set matching (top-k last-layer candidate SIDs)#591
Conversation
Residual quantizer and SID models emit top-k last-layer candidate codes and scores alongside the greedy SID at inference time, gated by SidCandidateOutputConfig (strategy 'last_layer_knn'): keep the greedy prefix and vary only the last layer across its nearest codes, best-first (slot 0 is the greedy code). These candidates feed downstream codebook collision handling. Also tidies the quantizer subsystem (drops the inert include_origin / redundant candidate-output flags). Proto adds SidCandidateOutputConfig. Includes unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…zer walk Follow-up to e22f2d8 (in-model candidate-set matching). Hardens and simplifies the candidate-output path with no change to the fitted/enabled behavior: - Fix: candidate output on an unfit RQ-KMeans codebook crashed at inference (_build_code_candidates hit a bare assert on the pre-fit layer's None top-k). Add a backend-overridable ResidualQuantizer._candidates_available() hook (KMeans -> is_fitted) folded into _should_output_candidates(), so an unfit codebook emits no candidates instead of crashing. Regression test added. - Wrap the _residual_pass 5-tuple return in a ResidualPassOutput NamedTuple. - Derive include_candidates internally from _should_output_candidates() instead of threading it through every caller. - Share the eval top-k QuantizeOutput builder as QuantizeLayer._topk_output() (via the base lookup()), removing the copy-paste between the two backends. - KMeans: argmin on the raw cdist (skip the wasted .pow(2) on the training path; square only the surviving eval scores). - Docstring/annotation fixups (Tuple, candidate_output_config Args). 149 SID tests pass; FX/export-trace tests unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings in upstream alibaba#551 (v1.3.0: torch 2.12.1 / torchrec 1.7.0 / fbgemm 1.7.0 / numpy 2 / cu130 TRT), alibaba#556 (pandas>=3), alibaba#554 (graphlearn sampler fail-fast), alibaba#552 (SID RQ-VAE/RQ-KMeans docs), alibaba#553 (CI runtime cuts). No conflicts — local work is confined to the SID quantizer subsystem; upstream touched requirements, docs, CI, and framework files that don't overlap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-review cleanup of the candidate-output subsystem; no behavior change. - Remove purely-descriptive and invariant-justification comments across the SID quantizers and models; keep docstrings and the external-gotcha comments (Sinkhorn overflow, STE codebook-freeze, DDP broadcast, cdist dtype, checkpoint poison, in-place buffer consume, python -O asserts, faiss gpu kwarg). - Delete 4 redundant tests, coverage preserved by lower-layer tests: topk-validation at the model level (already unit-tested in residual_quantizer_test), a train_offline-then-decode subset, and a duplicate candidate-score-ordering test. - Inline QuantizeLayer._check_topk into nearest_neighbors as a single guard and drop the method; merge its two range messages into one. 118 SID tests pass; ruff check + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ghten comments No behavior change. - Merge the two candidate-topk range checks in _init_candidate_output_config into one guard (topk must be in [1, n_embed]); update the 4 tests that asserted the old two messages. - Move self._candidate_layer_idx into _init_candidate_output_config, set only when candidate output is enabled. - Sweep remaining narration / "why-it's-correct" comments and tighten the kept external-gotcha comments (faiss gpu kwarg, DDP broadcast deadlock, STE codebook-freeze, cdist dtype, checkpoint poison, in-place consume, python -O, AMP seed) to one-liners. 118 SID tests pass; ruff check + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Simplify the SID unit tests; behavior and coverage unchanged (118 tests pass). - Add faiss_unavailable to tzrec/utils/test_util.py; replace the 12 try/except faiss-import skipTest blocks + the _require_faiss helper with the shared @unittest.skipIf(*faiss_unavailable) decorator. - Parameterize near-identical tests via parameterized.expand: candidate-config validation raises, build_candidates topk (1/2), and the train_offline guards. - Extract shared fixtures/asserts: _vq / _pinned_vq (vector_quantize), _candidate_rvq factory (residual_vector), _inference_fake (residual_quantizer), _build_contrastive / _assert_has_grad (rqvae), _collect / _fit / _assert_codes_valid + a candidate_topk param on _create_model (rqkmeans); a stub setUp; and the stub quantize now delegates to the base _topk_output. Net -119 lines; ruff check + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… topk default Addresses the PR alibaba#591 review findings (all low/nit). - _residual_output builds the (B, n_layers, D) latents stack only when a consumer needs it: RQ-VAE keeps it for the commitment loss at train/eval, while the inference path and K-Means (which never read latents) skip it. latents is now Optional on ResidualQuantizerOutput. - Remove the dead QuantizeOutput.scores field: _topk_output set it but nothing read it (candidate handling uses topk_scores). - SidCandidateOutputConfig.topk default 10 -> 1 so a minimal opt-in config (topk unset) always constructs, even when the last-layer codebook is < 10; set topk explicitly for a useful candidate set. 145 SID + loss tests pass; ruff check + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review summaryOverall a clean, well-tested refactor. The shared Posted a few inline notes (none blocking):
Two smaller items, not worth an inline comment:
|
The prior commit made ResidualQuantizerOutput.latents Optional, but _predict_rqvae/_predict_mixed feed it straight into a Dict[str, Tensor] value and torch.cat(), which pyre (pytyping_ci) rejects. The RQ-VAE train/eval path always builds latents, so bind a local and assert non-None to narrow the type at those two call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All six github-actions review comments (non-blocking) plus one follow-up: - sid_rqvae.predict docstring: note inference also emits candidate_codes/candidate_scores when candidate output is configured. - ResidualQuantizer class docstring: _quantize_layer is a concrete base method (delegates to each layer's quantize), not a subclass primitive. - _init_candidate_output_config: always define _candidate_layer_idx / _candidate_output_topk (last layer, greedy-only) so downstream reads stay safe when candidate output is disabled. - QuantizeLayer.nearest_neighbors docstring: it returns (scores, ids), not "ids and scores". - VectorQuantizeLayer.quantize: build the distance matrix under no_grad on the STE/eval paths (Gumbel keeps grad -- distances feed its logits); the STE codebook gradient still flows through the embedding lookup. Also fix the now-stale _compute_distances docstring that pointed at the old no_grad location. - ResidualKMeansQuantizer: import faiss lazily inside train_offline so the module and its tests load without faiss (forward/predict are faiss-free), making the @skipIf(faiss_unavailable) skips effective again. 145 SID + loss tests pass; ruff check + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5dd802b to
8ff3fbc
Compare
Empty commit advancing the PR head to the current (all-green) tip after the branch was briefly reverted to an ancestor and fast-forwarded back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the two-line explanatory comment on the offline-fit faiss import; keep the side-effect import and its `# noqa: F401` (the standard idiom for a registration-only import). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tag the SID candidate-matching feature release (one patch above the upstream 1.3.0 base). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| ub = torch.tensor(n_embed) # per-layer K, broadcast across columns | ||
| self.assertTrue(((out.cluster_ids >= 0) & (out.cluster_ids < ub)).all()) | ||
|
|
||
| @unittest.skipIf(*faiss_unavailable) |
There was a problem hiding this comment.
This skipIf(*faiss_unavailable) gate looks spurious: the test loads centroids directly via load_centroids_ and runs no FAISS fit, and since import faiss.contrib.torch_utils was moved into train_offline, construction/load/forward no longer need faiss. This is the only place the K-Means residual last-layer candidate ordering ([2, 3, 1]) and greedy-prefix invariance are asserted, so on a faiss-less CI runner the K-Means-specific candidate contract goes unverified. Recommend dropping the decorator so it runs unconditionally.
| None (K-Means and the inference path never read it). | ||
| candidate_codes (Tensor, optional): candidate code tuples, shape | ||
| (B, K, n_layers). | ||
| candidate_scores (Tensor, optional): candidate scores, shape (B, K). |
There was a problem hiding this comment.
Worth documenting the candidate_scores semantics more precisely, since this is a public inference output whose stated purpose is offline collision prevention: (1) the values are distances (lower = better, ascending), not similarity scores; and (2) they are the last-layer residual distance only and backend-scoped (squared-L2 for K-Means / L2-VQ, negative cosine for cosine-VQ), so they rank candidates within one item but are not directly comparable across items. A one-line caveat here (and/or in the proto) would prevent a downstream consumer from treating them as cross-item comparable scores.
| } | ||
|
|
||
| message SidCandidateOutputConfig { | ||
| reserved 4, 5; |
There was a problem hiding this comment.
reserved 4, 5; in a brand-new message with no prior released version reads as leftover from development iterations. Either add a short comment noting what 4/5 were (to prevent accidental reuse) or drop the reservation — as-is it will puzzle future readers.
Code review summaryReviewed across code quality, performance, tests, docs, and safety. This is a solid, well-structured PR — the
A few noteworthy items (inline comments posted for the specific-line ones): Test coverage gaps (all inference-only, low risk but worth closing):
Minor code quality: the refactor hoisted the encode primitive Nothing here is blocking. Nice work on the DRY tests and the thorough validation. |
- residual_kmeans_quantizer_test: drop @skipIf(faiss_unavailable) on test_candidate_output_last_layer_knn. It loads centroids directly (no train_offline / FAISS fit), so with the lazy faiss import it no longer needs faiss -- and it's the only assertion of the K-Means last-layer candidate ordering + greedy-prefix invariance, so it should run on faiss-less CI. - sid_model.proto: drop `reserved 4, 5;` from SidCandidateOutputConfig. Tags 4/5 were never real fields (present from the message's first commit, never released), so there is nothing to protect against reuse. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Adds inference-time candidate-set matching to the SID (semantic-ID) generation models. Alongside the greedy SID, the residual quantizers emit the top-k last-layer candidate SID tuples + scores, gated by a new SidCandidateOutputConfig (strategy "last_layer_knn"): keep the greedy prefix and vary only the last layer across its nearest codes, best-first (slot 0 == the greedy SID). These candidates feed downstream codebook-collision handling. Opt-in and inference-only.
Changes
Testing