Skip to content

[feat] SID: add sid_quality_report tool for offline collision/codebook stats#596

Merged
tiankongdeguiji merged 13 commits into
alibaba:masterfrom
WhiteSwan1:feat/sid_eval
Jul 14, 2026
Merged

[feat] SID: add sid_quality_report tool for offline collision/codebook stats#596
tiankongdeguiji merged 13 commits into
alibaba:masterfrom
WhiteSwan1:feat/sid_eval

Conversation

@WhiteSwan1

Copy link
Copy Markdown
Collaborator

What

Adds tzrec/tools/sid_quality_report.py — a single-process CLI that measures the quality of generated Semantic IDs (SIDs) over a whole item corpus. It is the exact, global complement to the per-batch tzrec/metrics/unique_ratio.py::UniqueRatio (which is batch-size-biased and never sees the full corpus at once).

It reads the codes column of a tzrec.predict output for a SID model and reports:

  • corpus summary (one row): no_collision_rate, collision_free_item_rate, max_collision, gini, entropy, max_entropy, entropy_ratio;
  • per-layer codebook table (one row per layer): coverage, dead_codes, perplexity.

Design

  • Framework I/O via create_reader/create_writer — CSV / Parquet / MaxCompute for both read and write; reads only the codes column (no dependency on predict --reserved_columns).
  • Counting via mixed-radix int64 SID ids + np.unique (no per-row Python Counter) — ~9x faster at 5M rows; overflow-guarded (product of codebook sizes must fit int64).
  • Entropy via torch.special.entr (~10x vs numpy on CPU, multithreaded); numpy elsewhere.
  • --codebook is required and validated; out-of-range codes are clamped with a one-shot warning.
  • Two all-scalar output tables, so both write natively to CSV/Parquet/ODPS.

Testing

Differentially tested against a Counter reference (200 random trials over L=1..4 codebooks + a 200k-row fixture): identical unique count / frequency distribution / decoded top-SIDs, ~9x faster counting. Edge cases verified: out-of-range clamping, int64 overflow guard, and the CSV string-codes path. ruff check + ruff format clean.

WhiteSwan1 and others added 7 commits July 8, 2026 09:22
…k stats

Reads the `codes` column of a tzrec.predict SID table and reports, over the whole
corpus: no_collision_rate, collision_free_item_rate, max_collision,
gini/entropy/max_entropy/entropy_ratio, and a per-layer codebook table
(coverage/dead_codes/perplexity). The exact global complement to the per-batch
UniqueRatio metric.

- framework create_reader/create_writer (csv/parquet/odps); reads only `codes`
- counting via mixed-radix int64 SID ids + np.unique (no per-row Counter;
  ~9x faster at 5M rows), overflow-guarded
- torch.special.entr for entropy (~10x vs numpy on CPU); numpy elsewhere
- --codebook required + validated
- scalar summary table + long-format per-layer table (csv/parquet/odps-safe)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fork state after merging alibaba/TorchEasyRec master (1.3.1): adds the
sid_quality_report SID-eval tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dd tests

- relocate tzrec/tools/sid_quality_report.py -> the tzrec/tools/sid/ subpackage
- harden malformed / out-of-range input (from code review): build_arr filters
  null rows/elements, wrong-width and non-integer rows per row (no whole-batch
  drop); out-of-range codes are skipped rather than clamped (clamping fabricated
  collisions); dropped rows are counted and reported (malformed vs out-of-range)
- crash-safe parse_codes: a non-integer string token no longer aborts the run
- raise ValueError (not SystemExit) on empty input
- add tzrec/tools/sid/sid_quality_report_test.py (unit + end-to-end, incl. a
  malformed/out-of-range regression case)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xample

The docstring Example used `\\` (escaped backslash) instead of a single `\`, so
copy-pasting it into a shell broke line continuation (each --flag ran as its own
command). Use single backslashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…quely_identified_item_rate

Clearer name for "fraction of items whose SID is theirs alone (count==1)",
and less confusable with no_collision_rate (= unique_sid / total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@WhiteSwan1 WhiteSwan1 added the claude-review Let Claude Review label Jul 13, 2026
@github-actions github-actions Bot removed the claude-review Let Claude Review label Jul 13, 2026
Comment thread tzrec/tools/sid/sid_quality_report.py Outdated
Comment thread tzrec/tools/sid/sid_quality_report.py Outdated
Comment thread tzrec/tools/sid/sid_quality_report.py
Comment thread tzrec/tools/sid/sid_quality_report_test.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Review summary

Solid, well-engineered tool. The mixed-radix int64 encoding → single terminal np.unique is sound, the overflow guard correctly keeps every id < capacity ≤ int64.max, docstrings are thorough and accurate (the uniquely_identified_item_rate rename is consistent throughout — no stale collision_free_item_rate left in code/comments), and the gini-over-count-histogram is a genuinely good optimization. Malformed/out-of-range handling is careful and the intent to drop rather than clamp is correct. Divide-by-zero paths are all guarded.

Noteworthy items posted inline: opaque KeyError on a wrong --codes_field; uncaught OverflowError on an out-of-int64 token in the string-codes path; the core pipeline being trapped under __main__ (blocks in-process unit tests of the overflow guard, --log_top_sids decode, entropy_ratio nan, and CSV/string paths); and an end-to-end test whose symmetric 16,16,16 codebook can't catch a radix-ordering bug.

Minor (non-blocking):

  • Peak memory: all_ids (list of per-row int64) stays alive through np.concatenate + np.unique (which flattens internally), ~3× the id array at the end. A del all_ids right after ids = np.concatenate(all_ids) sheds one full copy at the top of the advertised row range.
  • entropy_ratio denominator: normalized against log(capacity), so when total << capacity (the common case) the ratio can never approach 1.0 even for a perfectly uniform collision-free assignment. If intentional, a one-line note that entropy_ratio < 1 is expected whenever total < capacity would keep readers from misreading it as collisions.
  • Docs: other tzrec.tools.* entry points are referenced from docs/; a short invocation snippet in sid_rqkmeans.md/sid_rqvae.md (which already discuss the no-collision-rate target this tool computes) would help discoverability.

WhiteSwan1 and others added 5 commits July 13, 2026 07:06
…ndling

Move the scan/finalize core of sid_quality_report into an importable run_report(),
leaving only argparse/reader/writer wiring in __main__, so the numeric branches
(mixed-radix encode/decode, capacity guard, per-layer stats) are unit-tested
in-process instead of only through the subprocess CLI. Harden the comma-string
codes path: a blank field ("1,2,,3") or an int64-overflowing token is dropped as
malformed rather than silently compacted into a fabricated SID or crashing
np.asarray(int64); a mistyped --codes_field fails with a clean usage error instead
of a KeyError mid-scan. Tests switch to an asymmetric codebook that actually pins
radix ordering and per-layer indexing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tiankongdeguiji tiankongdeguiji added the codex-review Let Codex Review label Jul 14, 2026
@github-actions github-actions Bot removed the codex-review Let Codex Review label Jul 14, 2026
Comment thread tzrec/tools/sid/sid_quality_report.py
Comment thread tzrec/tools/sid/sid_quality_report.py
Comment thread tzrec/tools/sid/sid_quality_report.py
Comment thread tzrec/tools/sid/sid_quality_report.py
Comment thread tzrec/tools/sid/sid_quality_report_test.py
@github-actions

Copy link
Copy Markdown
Contributor

Static review complete. The mixed-radix encoding and malformed/out-of-range row handling are thoughtfully structured. I left five focused inline findings covering input-type correctness, histogram/top-N scaling, Gini interpretation, and cross-batch coverage.

Please also refresh the PR description before merge: it still points to tzrec/tools/sid_quality_report.py, says out-of-range codes are clamped, and names collision_free_item_rate; the implementation is now under tzrec/tools/sid/, drops out-of-range rows, and emits uniquely_identified_item_rate.

No tests or builds were run, per the static-review request.

…p_sids

build_arr accepted any Arrow list schema, so a list<float> codes column crashed
in np.bincount on the clean fast path yet was silently truncated to int64 on the
malformed-row fallback -- the result depended on batch cleanliness. It now
rejects non-integer element types up front and normalizes accepted ints to
int64. A negative --log_top_sids was truthy and sliced [:-1], logging almost
every SID; N is now validated as positive in both the CLI and run_report. Also
documents that Gini measures inequality among occupied SID buckets, and adds a
cross-batch aggregation test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tiankongdeguiji tiankongdeguiji added the codex-review Let Codex Review label Jul 14, 2026
@github-actions github-actions Bot removed the codex-review Let Codex Review label Jul 14, 2026
all_ids.append(arr @ radix)
layer_hist = update_layer_hist(layer_hist, arr, codebook)

if n_malformed or n_oob:

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.

n_malformed and n_oob only reach the log, while the saved summary calls the surviving count total. A wrong codebook can therefore drop much of the corpus yet persist healthy-looking global rates. Could we include input/skipped counts in the summary, or fail on out-of-range input?

)
parser.add_argument(
"--batch_size",
type=int,

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.

type=int accepts zero. Once the shared reader buffers data, a zero batch size repeatedly takes an empty slice without consuming that buffer, so --batch_size 0 can hang on nonempty input. Please reuse _positive_int here.

log_top_sids=args.log_top_sids,
)

if args.summary_output:

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.

Please reject output targets that overlap the input or each other before creating these writers. CSV/Parquet writers overwrite part-0 and ODPS uses overwrite mode, so an output in the input directory/table can destroy the source; equal output paths also make layer stats replace the summary.

@github-actions

Copy link
Copy Markdown
Contributor

Static review complete. I left three inline findings covering auditable skipped-row accounting, validation of non-positive batch sizes, and protection against output targets overwriting source data or each other. Other specialist suggestions were either already present on the PR or not material enough to repeat. No tests or builds were run.

@tiankongdeguiji
tiankongdeguiji merged commit 59f6269 into alibaba:master Jul 14, 2026
9 checks passed
@WhiteSwan1
WhiteSwan1 deleted the feat/sid_eval branch July 16, 2026 02:02
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