Skip to content

[feat] SID: in-model candidate-set matching (top-k last-layer candidate SIDs)#591

Merged
WhiteSwan1 merged 13 commits into
alibaba:masterfrom
WhiteSwan1:feat/sid_candidate_matching
Jul 8, 2026
Merged

[feat] SID: in-model candidate-set matching (top-k last-layer candidate SIDs)#591
WhiteSwan1 merged 13 commits into
alibaba:masterfrom
WhiteSwan1:feat/sid_candidate_matching

Conversation

@WhiteSwan1

@WhiteSwan1 WhiteSwan1 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

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

  • proto: SidCandidateOutputConfig (enabled / topk / strategy) on SidRqvae and SidRqkmeans.
  • QuantizeLayer: nearest_neighbors + a shared _topk_output builder (via the base lookup()), so both backends (VQ / K-Means) return the greedy pick plus the top-k ids/scores in eval from one place.
  • ResidualQuantizer (now a BaseModule, to pick up the framework is_inference mode flag via set_is_inference) owns the candidate walk (_build_code_candidates) and a ResidualPassOutput result type. Candidate emission is decided internally by _should_output_candidates() = inference mode + config-enabled + a backend _candidates_available() hook (K-Means -> is_fitted), so an unfit codebook emits no candidates instead of crashing.
  • SID models expose candidates via predict() at inference only.
  • Tidies the quantizer subsystem: unified _quantize_layer, a single ResidualPassOutput walk result, one combined topk range guard, and argmin on the raw cdist in the K-Means training path (dropped a needless .pow(2)).

Testing

  • Unit coverage for every piece (quantizers, models, losses); FX-symbolic-trace and export-trace tests cover the candidate path. 118 SID unit tests pass; ruff check + format clean.
  • Tests are kept DRY: a shared faiss_unavailable in test_util + @unittest.skipIf, parameterized validation/shape tests, and shared construction/assert helpers.

WhiteSwan1 and others added 7 commits July 6, 2026 02:45
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>
@WhiteSwan1 WhiteSwan1 added the claude-review Let Claude Review label Jul 7, 2026
@github-actions github-actions Bot removed the claude-review Let Claude Review label Jul 7, 2026
Comment thread tzrec/models/sid_rqvae.py
Comment thread tzrec/modules/sid/residual_quantizer.py
Comment thread tzrec/modules/sid/residual_quantizer.py
Comment thread tzrec/modules/sid/quantize_layer.py Outdated
Comment thread tzrec/modules/sid/vector_quantize.py Outdated
Comment thread tzrec/models/sid_rqkmeans_test.py
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review summary

Overall a clean, well-tested refactor. The shared _residual_passResidualPassOutput / _residual_output factoring is nicely done, candidate output is correctly gated on is_inference + enabled + _candidates_available(), candidate_codes[:, 0] provably equals the greedy SID, and latents is correctly skipped on the inference path. A security/DDP pass found no real issues: topk is double-bounded (construction + nearest_neighbors) so there's no OOB into a codebook, the K-Means unfit path degrades to "no candidates" instead of crashing, and the candidate walk is inference-only so it introduces no new collective op / rank divergence.

Posted a few inline notes (none blocking):

  • Two docstrings gone stale after the refactor — SidRqvae.predict ("codes only") and the ResidualQuantizer class doc (_quantize_layer is no longer a subclass primitive).
  • A minor training-path perf regression: the STE distance matrix is now built with autograd tracking and discarded each step.
  • A fragility in _init_candidate_output_config (candidate attrs left unset when disabled — safe only via short-circuit).
  • The new @skipIf(*faiss_unavailable) decorators don't actually make the K-Means test files faiss-optional (top-level faiss import fails collection first).

Two smaller items, not worth an inline comment:

  • Docs: the new candidate_output_config proto field isn't mentioned in docs/source/models/sid_rqvae.md / sid_rqkmeans.md, which otherwise enumerate every config field.
  • Coverage: the candidate FX/export-trace path is tested for RQ-VAE but not for the K-Means backend, whose eval branch differs (KMeansQuantizeLayer.quantize has its own if self.training split + distances.pow(2)). Since export is how this feature is consumed offline, a K-Means export-trace test would close that gap.

WhiteSwan1 and others added 2 commits July 7, 2026 09:51
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>
@WhiteSwan1
WhiteSwan1 force-pushed the feat/sid_candidate_matching branch from 5dd802b to 8ff3fbc Compare July 7, 2026 11:07
WhiteSwan1 and others added 3 commits July 8, 2026 02:09
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>
@tiankongdeguiji tiankongdeguiji added the claude-review Let Claude Review label Jul 8, 2026
@github-actions github-actions Bot removed the claude-review Let Claude Review label Jul 8, 2026
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)

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.

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).

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.

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.

Comment thread tzrec/protos/models/sid_model.proto Outdated
}

message SidCandidateOutputConfig {
reserved 4, 5;

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.

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.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Code review summary

Reviewed across code quality, performance, tests, docs, and safety. This is a solid, well-structured PR — the ResidualQuantizer base-class refactor is coherent, the candidate walk cleanly reuses the existing residual pass, and the feature is correctly gated (inference-only + config-enabled + backend _candidates_available). Verified specifically:

  • No training-path regression. _should_output_candidates() short-circuits on the plain-Python is_inference flag before any candidate work; train/eval keep topk=1 and skip the candidate block entirely. Inference also correctly skips the latents stack.
  • No correctness/safety issues. Config validation is double-layered (construction-time _init_candidate_output_config + call-time nearest_neighbors guard), fail-safe on unset config, and the unfit-K-Means path degrades to no-candidates rather than crashing. Checkpoint-integrity and DDP behavior are unaffected (candidate path is purely per-sample, no collectives).
  • Docs are accurate — shapes, defaults, and the "slot 0 == greedy" contract all match the implementation.

A few noteworthy items (inline comments posted for the specific-line ones):

Test coverage gaps (all inference-only, low risk but worth closing):

  • n_layers == 1 candidate path is untested — every candidate test uses n_layers=2/3. This is exactly the empty-prefix zero-width cat/expand case in _build_code_candidates where a shape/FX-trace regression would hide.
  • SidRqkmeans has no FX/export-trace test with candidates enabled (the CPU-only inference model), while SidRqvae does (test_rqvae_export_trace_keeps_candidate_output).
  • topk == n_embed (the exact upper boundary of the topk > n_embed guard) is never tested as a valid value.

Minor code quality: the refactor hoisted the encode primitive _quantize_layer into a concrete base method, but its decode mirror _lookup_code stays abstract with two byte-identical subclass overrides (return self.layers[layer_idx].lookup(code_idx)). It could be made concrete in the base too, symmetrizing the pair and removing the duplication.

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>
@WhiteSwan1
WhiteSwan1 merged commit 1b516cf into alibaba:master Jul 8, 2026
7 checks passed
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