Skip to content

feat(tools): dataset feature visualization tool - #3259

Open
DoubleCore wants to merge 18 commits into
antgroup:mainfrom
DoubleCore:issue-2862-viz-tool
Open

DoubleCore wants to merge 18 commits into
antgroup:mainfrom
DoubleCore:issue-2862-viz-tool

Conversation

@DoubleCore

Copy link
Copy Markdown

Implements #2862. Dataset feature visualization tool for pyvsag.

Fixes: #2862

Profile and visualize the feature characteristics of input vector
datasets — per-dimension value distributions, vector norms, sparsity,
duplicates, sampled pairwise distances, outliers, anisotropy, and an
optional clustering summary — for the dense and sparse HDF5 formats used
by pyvsag and the VSAG evaluation tooling.

- deterministic seeded sampling with bounded memory (no full-dataset
  O(N^2) pairwise matrix); full vs sampled metrics are tagged with
  definition, scope, and sample size
- versioned JSON report and a self-contained offline HTML visualization
- optional provider-neutral AI interpretation layer: environment-variable
  credentials, aggregate-only payload (raw vectors are never sent),
  schema-validated responses with evidence metric keys, and graceful
  degradation when the provider is unavailable
- unit and integration tests, reproducible toy HDF5 examples, and
  English/Chinese documentation

Fixes: antgroup#2862
Signed-off-by: DoubleCore <851678303@qq.com>
Assisted-by: OpenCode:qwen3.8-flash
@pull-request-size pull-request-size Bot added the size/XXL 1000+ changed lines label Nov 3, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-03T12:48:00.688874Z 6753077 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@vsag-bot
vsag-bot self-requested a review November 3, 2026 12:38
@vsag-bot

vsag-bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Reviewer | Review complete

Automated pull request review completed.
Review decision: Approved. No actionable findings were identified in this complete static review. CI and merge requirements remain separate.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (10632 changed lines across 34 files).

No actionable inline findings were found.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.
Review: #3259 (review)

@vsag-bot

vsag-bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Merger | Merge Readiness

Head: 40c880ab | Base: 6002c527 | Target: main

Updated: 2026-11-06 08:51:47 UTC

Status: BLOCKED

Merger decision: HOLD (shadow assessment; no merge executed).

  • PR CI Gate: failure
  • Conversations: 86 unresolved; resolution required
  • GitHub merge state: BLOCKED

Next owner: Author. Required next step: Inspect the required check failure and fix or verify its cause. Do not merge yet.

Merge reliability: 0.00 / 1.00 (WITHHELD)

Scoring basis Points Evidence
Required CI evidence 0.27 / 0.40 2/3 required check groups satisfy the branch policy from the expected source; neutral/skipped are not execution evidence
Branch review policy 0.00 / 0.20 GitHub review decision: REVIEW_REQUIRED; review conditions are not verified
Current AI review 0.40 / 0.40 Current head/base, complete static file coverage, correct verdict and zero P0/P1 findings

Evidence subtotal: 0.67; applicable ceiling: 0.00. Final score = min(subtotal, ceiling). Policy: merge-reliability-v2.

  • Ceiling 0.95: Static analysis and check outcomes do not prove correctness; v2 has an uncalibrated 0.95 policy ceiling.
  • Ceiling 0.00: GitHub merge conditions are BLOCKED; hard gates cannot be offset by other evidence.
  • Ceiling 0.95: Non-required checks have failed or unknown outcomes; Merger must establish relevance before recommending a merge.
  • Ceiling 0.50: 86 unresolved review threads remain.
  • Ceiling 0.85: The current AI review reports 3 residual risks; see its review for context.
Merge condition State Evidence
PR state PASS OPEN
Rules PASS Active rulesets and branch protection read
Conflicts PASS MERGEABLE
Merge method PASS squash
Merge commit freshness PENDING Waiting for GitHub to refresh the merge commit for the current head and base. Only current head checks are shown.
DCO PASS success
Mergify Merge Protections PASS success
PR CI Gate BLOCKED failure
Human review PENDING GitHub: REVIEW_REQUIRED; approvals >= 1; Code Owner approval required; stale approvals dismissed
Conversations BLOCKED 86 unresolved; resolution required
GitHub merge state BLOCKED BLOCKED

AI review: COMPLETE. Static review: patch is correct; CI is reported separately.

Review findings: 0; P0/P1 findings in that review: 0; coverage: 34/34 changed files. Findings are not a count of currently unresolved threads.

CI summary: 2 failure, 1 neutral, 14 skipped, 12 success. Check source: 40c880ab. Skipped/neutral checks are not evidence that tests executed.

Next action: Merge commit freshness: Waiting for GitHub to refresh the merge commit for the current head and base. Only current head checks are shown.; PR CI Gate: failure; Human review: GitHub: REVIEW_REQUIRED; approvals >= 1; Code Owner approval required; stale approvals dismissed; Conversations: 86 unresolved; resolution required; GitHub merge state: BLOCKED

CI check details
Check Source Result Required
CI Routing Tests PR CI (App 15368) success no
ci/circleci: path-filtering/filter StatusContext success no
ci/circleci: pull-request-check StatusContext success no
Community / ASan Build macOS arm64 PR CI (App 15368) skipped no
Community / ASan Build X86 PR CI (App 15368) failure no
Community / Build with TSAN PR CI (App 15368) skipped no
Community / Check Compatibility PR CI (App 15368) skipped no
Community / Coverage Configuration PR CI (App 15368) skipped no
Community / Detect Changes PR CI (App 15368) skipped no
Community / Docs PR CI (App 15368) success no
Community / Format Check PR CI (App 15368) success no
Community / Lite ${{ matrix.mode }} PR CI (App 15368) skipped no
Community / Python Build X86 PR CI (App 15368) skipped no
Community / Run TSAN Tests PR CI (App 15368) skipped no
Community / Shared Checks Result PR CI (App 15368) success no
Community / SIMD Coverage PR CI (App 15368) skipped no
Community / Test Example X86 PR CI (App 15368) skipped no
Community / Test Python X86 PR CI (App 15368) skipped no
Community / Test X86 Functests PR CI (App 15368) skipped no
Community / Test X86 Unittests PR CI (App 15368) skipped no
DCO CheckRun (App 1861) success yes
Issue Link PR Metadata (App 15368) success no
Mergify Merge Protections CheckRun (App 10562) success yes
Mergify Merge Queue CheckRun (App 10562) neutral no
PR CI Gate PR CI (App 15368) failure yes
Select checks PR CI (App 15368) success no
Select CI route PR CI (App 15368) success no
Summary CheckRun (App 10562) success no
Team PR CI (App 15368) skipped no

This is an uncalibrated evidence score, not a probability, correctness guarantee or approval. Zero means the assessment is withheld or lacks positive evidence, not a proven-bad patch. Automatic merging is disabled. New commits or base changes invalidate earlier evidence; never authorize a merge from this score alone.

@mergify

mergify Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 3 merge protections satisfied — ready to merge.

Show 3 satisfied protections

🟢 Require kind label

  • label~=^kind/

🟢 Require version label

  • label~=^version/

🟢 Require linked issue for feature/bug PRs

  • body~=(?im)(?:^|[\s\-\*])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+(?:#\d+|[\w.\-]+/[\w.\-]+#\d+|https?://github\.com/[\w.\-]+/[\w.\-]+/issues/\d+)

@vsag-bot vsag-bot added the kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 label Nov 3, 2026
@mergify mergify Bot added area/docs Website and repository documentation 网站与仓库文档 area/tools Command-line, evaluation, and developer tools 命令行、评测与开发工具 labels Nov 3, 2026
@vsag-bot

vsag-bot commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Jev-driven classification

Applied: kind/feature, area/docs, area/tools. Existing labels were preserved.

Option probabilities
Option Probability Result
kind/feature 100.0% Applied
area/tools 98.0% Applied
area/docs 88.0% Applied
area/testing 70.0% Not applied
module/datacell 58.0% Not applied
area/examples 57.0% Not applied
area/dependencies 56.0% Not applied
area/bindings-python 40.0% Not applied
module/index 16.0% Not applied
area/ci 6.0% Not applied
module/api 5.0% Not applied
module/attr 5.0% Not applied
module/simd 3.0% Not applied
area/bindings-node 2.0% Not applied
area/docker 2.0% Not applied
kind/bug 0.0% Not applied
kind/cleanup 0.0% Not applied
kind/documentation 0.0% Not applied
kind/enhancement 0.0% Not applied
kind/improvement 0.0% Not applied
No kind 0.0% Not applied

Model: jev-1.13.0. Selection threshold: 85%. Probabilities are model estimates, not guarantees.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (8360 changed lines across 34 files).
Submitted 8 inline comments.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit 6753077.

method="POST",
)
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] [P1] Reject unsafe redirects before forwarding bearer credentials

If the configured HTTPS endpoint responds with a 302 redirect to HTTP or another host, urllib's default redirect handler follows it and copies the Authorization header. The initial HTTPS check does not protect this subsequent request, so the API key can be exposed in cleartext or sent to an unintended origin. Reject HTTPS downgrades and prevent cross-origin credential forwarding.

}
metrics.update(
_sampled_metrics(
pair_matrix=pair_matrix,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Pass computed distances to the dense nearest-neighbor summary

Here pair_matrix is the M×D vector-coordinate array; the M×M result of _pairwise_dense was only used to obtain pair_values. _nearest_neighbor_metric consequently masks coordinate positions and takes coordinate minima. For [[0,0],[3,4],[6,8]], it summarizes [0,3,6] instead of Euclidean nearest-neighbor distances [5,5,5]. Retain and pass the computed distance matrix, as the sparse path does.

# Invalid (NaN/Inf) entries are excluded from numeric summaries and treated
# as zero, matching the semantics of the norms metric.
sample_matrix = np.where(np.isfinite(sample_matrix), sample_matrix, 0.0)
pair_matrix = sample_matrix[:pair_n]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Select pairwise rows uniformly before taking a prefix

sample_indices returns indices sorted by dataset position, so this prefix selects the lowest-index sampled rows rather than a uniform subsample. With 1000 rows and default settings, the pairwise phase always uses rows 0–511 regardless of seed, excluding the remaining rows entirely. Ordered or clustered datasets therefore produce biased distance and near-duplicate summaries. Randomly select the bounded subset before sorting for reads; apply the same correction to sparse sample_records[:pair_n].

return
index = ids[finite].astype(np.int64, copy=False)
vals = values[finite].astype(np.float64, copy=False)
np.add.at(self.count, index, 1.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Include implicit zeros in sparse dimension statistics

The accumulators count only explicitly stored finite entries, and arrays() passes them unchanged to _dimensions_metric. For two sparse vectors {0:2} and {}, dimension 0 is reported with mean 2, std 0, min 2, and zero ratio 0; the full-split values should be 1, 1, 0, and 0.5. Account for absent coordinates as zeros, while excluding explicitly invalid values, before calculating per-dimension summaries.

return
new_size = max(size, max(16, self.count.size * 2))
pad = new_size - self.count.size
self.count = np.concatenate([self.count, np.zeros(pad)])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Bound sparse accumulators independently of feature ID magnitude

A valid sparse record containing feature ID 2^31 makes _ensure allocate six float64 arrays of length 2^31+1, roughly 96 GiB before temporary copies, even if the file contains only one stored value. The later dense-sample guard cannot prevent this allocation. Use sparse keyed accumulators or a checked fallback that preserves other metrics when the inferred dimension exceeds a safe memory budget.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

已在当前 head 确认修复:_profile_sparse 在推断维度超过 _MAX_SPARSE_STATS_DIMENSION 时仅停用 per-dimension 统计并保留其他指标,_DimAccumulator._ensure 也有独立检查,不会按 2^31 分配数组。7a76bf70 上重新运行 test_sparse_huge_feature_id_skips_only_dimension_arrays 与 test_sparse_accumulator_rejects_id_driven_oversized_allocation,均通过。

method = "IQR fallback (MAD is zero)"
threshold = 3.0 * iqr
else:
mask = np.zeros(values.shape, dtype=bool)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Distinguish zero interquartile spread from identical norms

MAD and IQR can both be zero while extreme observations still exist. For 99 one-dimensional INT8 vectors with value 1 and one with value 100, this branch reports zero outliers and claims all norms are identical. Preserve detection of values outside the repeated central norm when both robust spread estimates vanish, instead of unconditionally clearing the mask.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

当前回退分支先用 np.all(values == median) 区分真正常量;MAD/IQR 都为零但存在偏差时使用 values != median,不再清零离群掩码。test_zero_mad_and_iqr_still_detect_extreme_norms 已在 7a76bf70 复测通过,99 个 1 加一个 100 会报告 1 个离群值。

"pair_count": int(pair_values.size),
}
median = float(np.median(finite))
threshold = max(0.0, median * float(epsilon))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Avoid interpreting unnormalized IP scores as duplicate distances

The default sparse metric is IP, whose distance 1−dot can be negative and need not vanish for identical vectors. Thus [2,0] and [2,100] have distance −3 and are labeled near-duplicates despite being far apart, while two identical [0.5,0] vectors have distance 0.75 and fail the default threshold. Use a duplicate-compatible distance for this indicator, enforce the necessary normalization, or mark it unavailable for unnormalized IP.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

dense 与 sparse 的 IP profiling 均已将近重复检测切换为欧氏距离,并在报告中明确 metric = euclidean,不再把 1-dot 当作非负重复距离。test_ip_near_duplicates_use_nonnegative_duplicate_distance 和 test_sparse_ip_near_duplicates_use_euclidean_distance 已在 7a76bf70 复测通过。

return None
if not math.isfinite(number):
return None
return round(number, 10)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Preserve small finite values during report serialization

Fixed rounding to ten decimal places turns every finite magnitude below about 5e-11 into zero. A valid FLOAT32 dataset with coordinates around 1e-12 consequently reports zero norms, means, and distances, and its distinct histogram edges collapse to zero. This affects JSON, HTML, and AI interpretation. Preserve floating-point values or round by significant digits rather than an absolute decimal scale.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6753077740

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
metrics.update(
_sampled_metrics(
pair_matrix=pair_matrix,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pass distances rather than raw vectors to dense NN statistics

For dense inputs with at least two sampled vectors, pair_matrix is still the M×D feature matrix, but _sampled_metrics() forwards it to _nearest_neighbor_metric(), which expects an M×M distance matrix and takes row minima after masking the diagonal. Consequently, the nearest-neighbor statistics summarize feature coordinates rather than distances; the committed Euclidean example already reports a nearest-neighbor minimum of -858.9626464844. Retain the result of _pairwise_dense() and pass that distance matrix here.

Useful? React with 👍 / 👎.

Comment on lines +493 to +495
np.add.at(self.count, index, 1.0)
np.add.at(self.sum, index, vals)
np.add.at(self.sumsq, index, vals * vals)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include implicit zeros in sparse per-dimension statistics

For sparse inputs, coordinates absent from a record are zeros, but these accumulators count only explicitly stored finite values, and _dimensions_metric() divides by that occurrence count. A coordinate containing 10 in one of ten vectors therefore reports mean 10, standard deviation 0, and zero ratio 0, instead of 1, 3, and 0.9; minimum values and entirely absent coordinates are also wrong. Include implicit zeros when finalizing the per-dimension counts and aggregates, while continuing to exclude actual NaN/Inf entries.

Useful? React with 👍 / 👎.

method="POST",
)
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict redirects before forwarding the AI authorization header

If the configured HTTPS endpoint returns a 301/302/303 redirect to HTTP or another origin, this default urlopen() call follows it with the bearer token intact. I verified that Python 3.14's HTTPRedirectHandler.redirect_request() preserves Authorization for both downgrade and cross-origin redirects. The constructor's HTTPS check therefore protects only the initial request, allowing a provider or reverse-proxy redirect to disclose the key despite the advertised HTTPS-only credential policy. Reject unsafe redirects before forwarding credentials, or disable automatic redirects.

Useful? React with 👍 / 👎.

# Invalid (NaN/Inf) entries are excluded from numeric summaries and treated
# as zero, matching the semantics of the norms metric.
sample_matrix = np.where(np.isfinite(sample_matrix), sample_matrix, 0.0)
pair_matrix = sample_matrix[:pair_n]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Randomize the pairwise subset instead of taking sorted prefixes

When the pairwise limit is smaller than the main sample, as with the defaults, sample_indices() has already sorted the selected indices, so this prefix is biased toward earlier file rows rather than being a uniform subsample. For a 1024-row dataset, the default pairwise calculation uses rows 0–511 regardless of the seed. Files ordered by cluster or time consequently produce biased distance and near-duplicate estimates. Draw a seeded random subset of sample positions in both the dense and sparse paths instead of taking their ascending prefixes.

Useful? React with 👍 / 👎.

Comment on lines +562 to +564
mask = np.zeros(values.shape, dtype=bool)
method = "all norms identical; no outlier can be distinguished"
threshold = 0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Distinguish zero robust spread from identical norms

When most norms are identical but a few are extreme—for example, 99 norms of 1 and one of 1000—both MAD and IQR are zero even though the norms are not all identical. This branch then reports zero outliers and incorrectly labels the dataset as having identical norms, hiding the very norm spikes the metric is intended to detect. Distinguish genuinely constant data from a zero-spread majority and handle the remaining deviations explicitly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

该分支现已区分“所有 norm 相同”和“中心值重复但稳健离散度为零”;后者显式标记所有偏离 median 的值。test_zero_robust_spread_detects_deviations_on_both_sides 覆盖 99 个中心值及两端极端值,并已在 7a76bf70 复测通过。

@vsag-lgtm vsag-lgtm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review @ 67530777 — 3 inline suggestion(s).

while True:
if offset + _LEN_SIZE > len(buffer):
break
(length,) = _LEN_STRUCT.unpack_from(buffer, offset)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[warning] Trusted record header length can force oversized allocations in the sparse byte-stream reader.

The no-offsets iterator only verifies that offset + record_size fits inside the current buffer before calling _decode_one_record. A malformed file can therefore declare a large length as long as the header-derived record_size is still within the chunk, and _decode_one_record will then attempt np.frombuffer(..., count=length) for ids and values before any further checks. This can allocate far more memory than the dataset actually contains. Consider capping length against the remaining bytes in the stream (e.g. len(buffer) - offset - _LEN_SIZE) before deriving record_size.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

no-offsets reader 现在会在等待完整 chunk 和调用 _decode_one_record 之前,将 header 推导的 record_size 与整个 byte stream 的实际剩余字节数比较;不可能的长度直接抛出 DatasetFormatError,不会进入 np.frombuffer 分配,同时仍支持合法记录跨 chunk。test_sparse_impossible_length_is_rejected_before_buffering 已在 7a76bf70 复测通过。

position = 0
while position < total_bytes:
end = min(position + max_chunk_bytes, total_bytes)
buffer += bytes(np.asarray(dset[position:end]).tobytes())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[suggestion] Accumulating the entire sparse byte stream into a single bytes object raises peak memory for large files.

Every chunk is appended to buffer with buffer += bytes(...), which recreates the backing string on each iteration. When offsets are absent, the process can retain memory proportional to the full file size, undermining the streaming goal. Consider keeping a small residual window (for example with memoryview over the latest chunk plus any trailing partial record) instead of merging all previously consumed bytes.

html_path = _default_output_path(args.input, "html")
else:
html_path = None
if html_path is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[note] Explicit --out-json without --out-html silently disables HTML generation, which differs from the help text default.

The help string says --out-html defaults to <input>.profile.html, but this branch only falls back to that path when --out-json is also omitted. If this is intentional behavior, consider updating the help message so users do not expect an HTML report alongside a JSON-only request.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

--out-html 帮助文本现已明确说明:单独指定 --out-json 而未指定 --out-html 时会省略 HTML;只有两者都省略时才使用默认 HTML 路径。test_help_mentions_key_flags 已在 7a76bf70 复测通过。

…group#2862)

Signed-off-by: DoubleCore <851678303@qq.com>
Assisted-by: OpenCode:ark-code-latest
@vsag-bot
vsag-bot self-requested a review November 4, 2026 04:05

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.
Review decision: Comment only. No approval was submitted.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (8644 changed lines across 34 files).
Submitted 9 inline comments.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit 96a2882.

"split": report.get("input", {}).get("split"),
"dataset": report.get("dataset"),
"config": report.get("config"),
"metrics": report.get("metrics"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Exclude per-vector projections from the aggregate AI payload

Forwarding all metrics includes clustering.projection_points and labels, even when clustering is disabled. For a one-dimensional split fully covered by the sample, the first PCA coordinate is each original value minus its mean, up to sign; the accompanying mean and min/max allow the original values to be recovered. Thus --ai sends recoverable per-vector data despite the documented aggregate-only boundary. Build an aggregate allowlist that omits per-vector projections and assignments.

if metric == "euclidean":
squared_norms = np.einsum("ij,ij->i", matrix, matrix)
squared = (
squared_norms[:, None] + squared_norms[None, :] - 2.0 * (matrix @ matrix.T)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Compute Euclidean distances without catastrophic cancellation

For valid FLOAT32 rows [[1e10, 0], [1e10, 1], [1e10, 2]], the norm and dot-product terms round to 1e20 in float64, so this subtraction returns zero for every pair instead of distances 1, 1, and 2. This corrupts pairwise and nearest-neighbor statistics and marks all pairs as near-duplicates. The sparse Euclidean branch has the same cancellation. Use bounded direct-difference calculations or a stable fallback for nearly cancelling terms.

y_values = [value for _, value in pairs]
y_min, y_max = min(y_values), max(y_values)
if math.isclose(y_min, y_max):
y_min -= 1.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Use representable padding for large constant chart ranges

A valid FLOAT32 split [[1e20, 0], [0, 2e20], [2e20, 1e20]] has identical per-dimension means near 1e20. Adding or subtracting 1.0 at that magnitude leaves the float64 bounds unchanged, so sy() divides by zero. The default CLI consequently fails after writing JSON and never produces HTML. Expand constant ranges using scale-relative, representable padding.

norms = np.linalg.norm(matrix, axis=1)
safe = np.where(norms > 0, norms, 1.0)
normalized = matrix / safe[:, None]
gram = normalized @ normalized.T

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Avoid a full-sample matrix for the mean cosine summary

_anisotropy_metric passes the entire sample here, bypassing pairwise_sample_size. For example, --sample-size 100000 --pairwise-sample-size 512 on 16-D data reaches an 80 GB Gram allocation, followed by additional quadratic triangle arrays, although the configured distance matrix is only about 2 MB. Compute the mean from normalized-vector sums and diagonal contributions, or use bounded blocks, so increasing the reduction sample does not unexpectedly exhaust memory.

vectors_with_invalid = 0
all_zero_vectors = 0

for _, block in dataset.iter_dense_blocks(block_rows=65536):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Bound dense blocks by their working byte size

The fixed 65536-row block makes streaming memory scale excessively with dimensionality. At 4096 FLOAT32 dimensions, one block alone retains 1 GiB of raw values, 2 GiB of converted values, and 256 MiB of finite flags before temporary arrays; subsequent iterations can retain previous working arrays during replacement. Small sampling settings cannot reduce this cost. Derive block_rows from a byte budget that accounts for the float64 working buffers.

"full dataset",
),
("Element type", _e(str(dataset.get("dtype"))), "full dataset"),
("Distance metric", _e(str(dataset.get("distance_metric") or "n/a")), "profiling metric"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] [P3] Display the effective profiling metric in the overview

dataset.distance_metric preserves the original file attribute, whereas config.metric holds the normalized metric actually used. Profiling a euclidean file with --metric angular therefore labels the overview's 'profiling metric' as euclidean while displaying angular-distance results; files using a fallback show n/a. Read the effective metric from report.config for this card.

% (color, points)
)
for index, value in pairs:
tip = "dimension %d: %.6g" % (index, value)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] [P3] Label downsampled points with their source dimension ranges

These indices refer to bucket positions after stats.downsample, not original dimensions. For a 4-D dataset with --max-dimension-points 2, the two values average dimensions 0–1 and 2–3, but the tooltips identify them as dimensions 0 and 1 and the Coordinate axis ends at 1. Use source bucket ranges or centers for coordinates and tooltips so users can locate the dimensions represented by each plotted value.

"<script>%s</script>\n"
"</body>\n</html>\n"
) % (
_e(dataset.get("file_name") or "dataset"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] [P3] Read the report filename from the input section

build_report stores file_name under report.input, and never puts it in report.dataset. Consequently every generated page title and header falls back to 'dataset', as the bundled HTML demonstrates, losing the input's identity when reports are compared or shared. Use report.input.file_name here and in _header.

"count": 32,
"max": 2.3593201637,
"mean": -27.5290439874,
"min": -858.9626464844,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] [P3] Regenerate the bundled reports with the current profiler

This Euclidean report contains a nearest-neighbor minimum of -858.96 and negative median, which the current non-negative Euclidean distance implementation cannot produce. The accompanying HTML repeats these values, and the sparse artifacts still omit implicit zeros and describe the old prefix sampling method. Regenerate all four bundled artifacts so the documented reproducible examples reflect the checked-in algorithm.

Distance metrics: compute dense Euclidean distances on directly shifted
vectors when the Gram expansion is numerically unstable, and use direct
differences for the sparse path, removing catastrophic cancellation that
produced large negative nearest-neighbor distances.

Memory: bound dense block working sets by a byte budget and chunk the
mean pairwise cosine computation via the sum identity.

AI payload: restrict aggregated metrics to an explicit allowlist so raw
cluster labels and projection points are never uploaded.

HTML report: derive the page title from the input file name, label charts
with the source bucket boundaries, and only pad curves to representable
values.

Also regenerate the bundled example artifacts, sync the docs, and add
regression tests for each fix.

Signed-off-by: DoubleCore <851678303@qq.com>
Assisted-by: OpenCode:deepseek-v4.1-flash
@vsag-bot
vsag-bot self-requested a review November 4, 2026 05:08

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.
Review decision: Comment only. No approval was submitted.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (9050 changed lines across 34 files).
Submitted 4 inline comments.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit 6f71418.

array = array[np.isfinite(array)]
if array.size == 0:
return {"bins": [], "counts": [], "bin_count": 0}
counts, edges = np.histogram(array, bins=int(bins))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Handle large constant values before constructing histogram bins

A valid FLOAT32 dataset containing a single vector [260, 0] reaches this call with a constant norm of 260. NumPy's automatic constant-range expansion by ±0.5 rounds back to the same float64 value at that magnitude, so it cannot construct usable equal-width bins and raises. Because _norms_metric calls histogram unconditionally, profiling aborts before either report is produced. Use representable magnitude-aware padding or explicit edges for constant ranges.

) -> Dict[str, Any]:
with np.errstate(divide="ignore", invalid="ignore"):
mean = np.where(count > 0, total / count, np.nan)
variance = np.where(count > 0, total_sq / count - mean * mean, np.nan)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Accumulate dimension variance without subtracting large moments

The E[x²] - E[x]² formula can erase genuine variation even for supported FLOAT32 inputs. For one coordinate containing 255 copies of 230 and one value 230 + 128, both terms round to the same float64 number, yielding std=0 although the population variance is 63.75. This incorrectly presents a varying coordinate as constant in JSON, charts, and variance rankings. Use stable centered or online second-moment accumulation in both dense and sparse paths.

def source_range(index: int):
if not downsampled:
return index, index
start = index * bucket_size

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Reconstruct the actual downsampling boundaries

stats.downsample uses integer linspace boundaries, whose bucket widths differ when source_length is not divisible by the output length. Multiplying by the ceiling bucket_size does not recover those boundaries: with five dimensions and --max-dimension-points 2, values summarize dimensions 0–1 and 2–4, but this code labels them 0–2 and 3–4. Larger inputs can even receive nonexistent coordinate labels. Preserve exact boundaries or reconstruct the same linspace partition for positions and tooltips.

matrix @ matrix.T
)
scale = squared_norms[:, None] + squared_norms[None, :]
unstable = (scale > 0) & (squared <= _EUCLIDEAN_GRAM_REL_TOL * scale)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Exclude self-distances from the instability mask

Each ordinary nonzero row has a zero diagonal distance and positive diagonal scale, so its self-pair satisfies this instability test. Consequently every row enters the fallback loop even when all distinct pairs are well conditioned. A 512-row, 4096-dimensional sample allocates and scans roughly 8 GiB of difference buffers after already computing the Gram matrix; dense IP profiling also takes this path for duplicate detection. Clear the diagonal before selecting unstable rows so stable inputs retain the fast path.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

_dense_euclidean 现在在选择 unstable rows 之前执行 np.fill_diagonal(unstable, False),自距离不会触发直接差值回退。test_dense_euclidean_skips_direct_fallback_for_self_distances 通过跟踪 np.einsum 确认稳定输入只执行初始 norm reduction,已在 7a76bf70 复测通过。

Signed-off-by: DoubleCore <doublecore@users.noreply.github.com>
Assisted-by: OpenCode:ark-code-latest
@vsag-bot
vsag-bot self-requested a review November 4, 2026 11:15
@DoubleCore

Copy link
Copy Markdown
Author

修复已推送 a644c7db。本轮处理:

  • 稀疏 per-dimension 统计纳入隐式 0;累加器不再按最大 feature ID 扩容;
  • 区分「稳健离散度为 0」与「所有值相同」,不再把 MAD/IQR=0 当作分布退化;
  • IP 度量下不再把 1−dot 当重复距离;
  • 直方图常量区间 / 稳定方差 / 非均匀降采样边界 / 欧氏距离自配对回退等边界行为修正。
    验证:uv run --with numpy --with h5py --with pytest python -m pytest tools/dataset_profiler/tests -q → 106 passed(基线 97,新增 9 条边界用例)。

…ples from current serializer

Signed-off-by: DoubleCore <851678303@qq.com>
Assisted-by: OpenCode:ark-code-latest
@vsag-bot
vsag-bot self-requested a review November 5, 2026 01:42

@vsag-lgtm vsag-lgtm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review @ 663e0f47 — 3 inline suggestion(s).

"""Estimate the bytes one sampled sparse row keeps after retention and
after the Python dict expansion performed by :func:`_pairwise_sparse`."""
return (
_SPARSE_SAMPLE_ROW_OVERHEAD_BYTES + int(ids.size) * _SPARSE_SAMPLE_ENTRY_BYTES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[warning] The 32-byte per-entry estimate under-counts the Python dict expansion that _pairwise_sparse performs, so this guard does not actually bound peak memory at _SAMPLE_MEMORY_BUDGET_BYTES (256 MiB).

_pairwise_sparse (profiler.py:1486) materialises {int(key): float(value) ...} for every sampled pair row, which costs roughly 104-108 bytes per entry on CPython (52 B dict slot + 28 B int + 24 B float); I measured ~108 B/entry on CPython 3.12. The retained ids/values arrays need only 8 B/entry, so of the 32 bytes charged here about 24 are left to pay for a ~108-byte expansion.

With the default budget the check admits ~8.4M retained entries. If pairwise_sample_size equals sample_size all of those rows expand into dicts (~900 MiB); even the default 512/1024 split still reaches ~450 MiB, and _sparse_sample_to_dense can add another 256 MiB on top. That is precisely the nnz-heavy case the guard is meant to keep in bounds.

Suggestion: raise _SPARSE_SAMPLE_ENTRY_BYTES (profiler.py:58) to cover the dict entry (>= 112) and/or charge the dense expansion against the same running total instead of a separate _SAMPLE_MEMORY_BUDGET_BYTES check in _sparse_sample_to_dense (profiler.py:1567).

duplicate_metric,
int(pair_matrix.shape[0]),
duplicates_reason,
near_unavailable_reason=sample_budget_reason,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[suggestion] near_unavailable_reason makes the report contradict itself when both budgets trip at once.

When this budget stops sample retention and hashes is None (full-split duplicate state budget exceeded), _duplicates_metric still emits the note at profiler.py:1137-1141 — "... the near-duplicate indicator still covers the bounded pairwise sample" — while near_duplicate now carries available: false together with the budget reason passed in here. Every other consumer of that field is guarded, so please select the note based on whether near_unavailable_reason is set, and in that case state that both the exact and the near-duplicate indicators are unavailable.

hashes[index] = np.frombuffer(_digest(payload), dtype=np.uint8)
if sample_budget_reason is None and index in sample_set:
row_bytes = _sparse_sample_row_bytes(ids)
if retained_sample_bytes + row_bytes > _SAMPLE_MEMORY_BUDGET_BYTES:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[suggestion] The report config still advertises the un-reduced sample size after this guard trips.

profile_dataset (profiler.py:185-210) only lowers the effective sizes through _dense_sample_size, so for a sparse dataset that trips this budget the config keeps e.g. effective_sample_size: 1024 and effective_pairwise_sample_size: 512 while all four sampled metrics and duplicates.near_duplicate report sample_size: 0 with available: false. The dense path instead reduces effective_sample and emits a dedicated warning. Recording the retained row count (in the config, or at least in the warning text) would keep the config and the metric entries consistent for report consumers.

… boundary checks

Signed-off-by: DoubleCore <851678303@qq.com>
Assisted-by: OpenCode:ark-code-latest
@DoubleCore

Copy link
Copy Markdown
Author

已在提交 9b4a59c0da1445a4510a3d34f2521d29a72cdda3 中修复这两条 P2:

  1. profiler.py:稀疏采样预算计入 Python 对象与 dict 开销

    • 将每条目的保守估算从 32 B 调整为 128 B,覆盖保留的 (uint32, float32) 数组、_pairwise_sparse 展开时创建的 Python int/float 对象,以及 dict 哈希表存储开销。
    • 该估算仍在保留采样行和进行 dict 展开之前执行;nnz 过高的采样会进入 sampled metrics unavailable 路径,而不是按过低的估算继续展开。
    • 更新了原有内存预算回归测试,确认超预算时不调用 _pairwise_sparse,恢复默认预算后稀疏 pairwise/anisotropy 仍正常。
  2. hdf5.py:超大 offset 索引保留记录边界校验

    • 顺序解码 fallback 现在只读取有界 offset 窗口(复用 _OFFSET_VALIDATION_ROWS),并在产出每条记录前,将解码出的 [start, end) 与磁盘上的两个 offset 边界逐一比对;原有 decoded record count 校验仍保留。
    • 新增回归测试:仅将一个内部 offset 加 1,保持数量、单调性和首尾端点不变,强制走 oversized-index fallback 后会在第一条受影响记录处抛出 DatasetFormatError。

本地验证:

uv run --with pytest --with numpy --with scipy --with h5py python -m pytest tools/dataset_profiler -q
148 passed in 1.85s

即 148 passed / 0 failed,包含稀疏采样、超大 offset 正常路径及新增损坏边界回归测试。本地未编译 C++,仅运行了上述 Python 侧测试;此评论不表示 CI 已通过。

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.
Review decision: Comment only. No approval was submitted.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (10448 changed lines across 34 files).
Submitted 3 inline comments.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit 9b4a59c.

"schema_version": report.get("schema_version"),
"tool": report.get("tool"),
"split": report.get("input", {}).get("split"),
"dataset": report.get("dataset"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Filter dataset metadata before sending it to the provider

This forwards dataset.datasets, which hdf5.py populates from every top-level HDF5 object name. Consequently, profiling train with --ai also transmits names of unrelated objects, including any customer identifiers or private labels embedded in those names. This exceeds the documented aggregate-only, bounded non-sensitive metadata boundary. Allowlist the necessary dataset properties and omit arbitrary object names.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 27be0fe. aggregate_payload now allowlists only vector type, dtype, count, dimensionality/inference, distance metric, and offsets presence. Arbitrary HDF5 object names (dataset.datasets) and the attributes map are no longer forwarded. Added a regression test with private-looking object/attribute values and verified neither appears in the provider prompt.

effective_sample = _dense_sample_size(
dataset_count, int(metadata.dim or 0), requested_sample
)
effective_pairwise = min(requested_pairwise, effective_sample)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] [P3] Preserve actual sample counts after sparse budget exhaustion

When sparse sample retention exceeds its budget, _profile_sparse disables sampled work and supplies a 0-by-0 pairwise matrix, but this recomputation still advertises the requested dataset-limited counts. For example, 1024 sparse rows with 2048 entries each exceed the default retention budget, yet config and the HTML header report effective samples of 1024 and 512 although the sampled metrics report zero. Propagate the actual effective counts from the sparse profiling result.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 27be0fe. _profile_sparse now returns the actual effective sample and pairwise counts, and profile_dataset uses those values in the report config consumed by the HTML header. Budget exhaustion therefore reports 0/0 instead of the requested counts; the normal sparse path retains its actual counts. The sparse budget test now covers both cases and also ensures no misleading dense-budget warning is emitted.

edges = edges[::-1]
counts, edges = np.histogram(array, bins=edges)
else:
counts, edges = np.histogram(array, bins=bin_count)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] [P3] Handle narrow nonconstant histogram ranges

Valid FLOAT32 rows [1, 0] and [1, 2**-25] produce float64 norms 1 and 1 + 2**-51. These unequal values bypass the padding branch, so this call requests 20 bins across a range containing only two float64 ULPs. The edges necessarily repeat, and JSON/HTML expose zero-width intervals as separate histogram buckets. Apply representability padding to narrow nonconstant ranges too, or reduce the bin count.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 27be0fe. Narrow nonconstant ranges now reduce the histogram bin count until NumPy produces strictly increasing edges. This handles both NumPy behaviors: raising for too many finite-sized bins and returning repeated edges. Added the reported 1.0 / 1.0 + 2**-51 regression case and assertions that all intervals have positive width and both samples are retained.

@vsag-lgtm vsag-lgtm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review @ 9b4a59c0 — no inline suggestion(s); incremental changes since the previously reviewed commit are clean.

Signed-off-by: DoubleCore <851678303@qq.com>
Assisted-by: OpenCode:ark-code-latest
@vsag-bot
vsag-bot self-requested a review November 5, 2026 03:31
@DoubleCore

Copy link
Copy Markdown
Author

Validation for 27be0fe: uv run --with pytest --with numpy --with scipy --with h5py python -m pytest tools/dataset_profiler -q completed with 150 passed; git diff --check also passed. C++ was not compiled locally. This update contains no C++ changes and received static source review only, plus the Python tests above.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.
Review decision: Comment only. No approval was submitted.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (10518 changed lines across 34 files).
Submitted 1 inline comment.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit 27be0fe.

distances[zero_norms, :] = 1.0
distances[:, zero_norms] = 1.0
distances[np.ix_(zero_norms, zero_norms)] = 0.0
distances[np.abs(distances) <= _ANGULAR_ZERO_TOL] = 0.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Preserve small positive angular distances before thresholding

For FLOAT32 rows [1,0] and [1,2**-24], the angular distance is approximately 1.78e-15, but this clamp replaces it with zero because the tolerance is approximately 7.1e-15. With both rows sampled, the report therefore counts the pair as a near duplicate even when near_duplicate_relative_epsilon=0; the default relative threshold also incorrectly reports 100%. Recompute small distances from normalized coordinate differences or restrict zero canonicalization to equivalent directions. The sparse angular branch has the same issue.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in a7d3805. The dense and sparse angular branches now recompute only near-zero Gram-based distances as half the squared normalized coordinate difference, so the reported FLOAT32 pair retains its positive 2**-49 distance instead of being clamped to zero; exactly identical normalized vectors still produce zero. Added dense/sparse regressions for both epsilon=0 and the default relative threshold. Validation: uv run --with pytest --with numpy --with scipy --with h5py python -m pytest tools/dataset_profiler -q (152 passed). No C++ was compiled locally; this Python-only change received static inspection plus Python tests.

@vsag-bot
vsag-bot self-requested a review November 5, 2026 03:59

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.
Review decision: Comment only. No approval was submitted.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (10570 changed lines across 34 files).
Submitted 1 inline comment.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit a7d3805.

for row in np.unique(np.nonzero(unstable)[0]):
columns = np.nonzero(unstable[row])[0]
differences = normalized[row] - normalized[columns]
direct = 0.5 * np.einsum("ij,ij->i", differences, differences)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Preserve zero angular distance for positive scalar multiples

For an angular dataset containing [[1,1],[3,3],[3,3]], independently rounded normalizations differ by one float64 ULP. This fallback assigns approximately 1.23e-32 to two mathematically zero distances. _near_duplicate_summary then multiplies that median by the default 1e-3, reporting only one near-duplicate pair instead of three. The sparse angular branch has the same issue. Handle normalization roundoff for exactly proportional vectors before applying the relative threshold, while preserving genuinely small positive angular distances.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 4a6ba2a. Both dense and sparse angular paths now canonicalize vectors by their largest absolute coordinate before computing norms. Exactly positive scalar multiples therefore have identical scaled coordinates and zero distance rather than normalization residue, while the direct-difference fallback still preserves genuinely small positive distances. Added dense/sparse regressions for [[1,1],[3,3],[3,3]], including all three near-duplicate pairs; the existing small-positive-distance regression also passes. Validation: uv run --with pytest --with numpy --with scipy --with h5py python -m pytest tools/dataset_profiler -q -> 154 passed. 本地未编译 C++,仅静态核对 + Python 测试;本次仅修改 Python。

Signed-off-by: DoubleCore <doublecore@users.noreply.github.com>
Assisted-by: OpenCode:ark-code-latest
@vsag-bot
vsag-bot self-requested a review November 5, 2026 04:20

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.
Review decision: Comment only. No approval was submitted.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (10606 changed lines across 34 files).
Submitted 2 inline comments.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit 4a6ba2a.

limit = int(offsets[start]) + int(max_chunk_bytes)
end = int(np.searchsorted(offsets, limit, side="right")) - 1
if end <= start:
end = start + 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Bound oversized sparse records before materializing them

When one record exceeds max_chunk_bytes, forcing end = start + 1 makes the following slice read the entire record anyway. A valid record with 100 million unique IDs occupies 800,000,004 bytes; decoding copies its arrays, and profiling allocates sorting and float64 buffers before checking the sample budget. This can exhaust several gigabytes despite the default 64 MiB chunk limit and sample_size=1. The sequential decoder likewise buffers a complete oversized record. Enforce a record working-memory limit before allocation, or process such records incrementally.

)

dset = f[self.split]
dtype = _describe_dtype(dset.dtype)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] [P2] Reject non-byte storage for sparse splits

_describe_dtype accepts FLOAT32 for sparse inputs, although both sparse decoders assume shape[0] counts bytes. For example, type='sparse' with train=np.zeros(2, dtype=np.float32) and no offsets is accepted and reported as two empty vectors by reinterpreting the floats' eight bytes. Adding byte offsets instead fails validation because the sentinel is compared with the two-element shape. Require one-byte integer storage for sparse splits so unsupported layouts are rejected instead of producing offset-dependent or misleading profiles.

@vsag-lgtm vsag-lgtm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review @ 4a6ba2ac — 2 inline suggestion(s); no blocking issues found in this increment.

digest_rows = np.ascontiguousarray(hashes).view(
np.dtype((np.void, _DUPLICATE_DIGEST_BYTES))
).reshape(-1)
_, group_sizes = np.unique(digest_rows, return_counts=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[suggestion] _full_split_state_reason admits this call when the digest buffer fits _FULL_SPLIT_STATE_BUDGET_BYTES at _DUPLICATE_BYTES_PER_VECTOR = 16 bytes per vector, but np.unique needs more than that input: a sorted copy of the digests (16 B/vector), the sort index working set (8 B/vector), the unique output (16 B/vector) and the counts (8 B/vector). Peak is therefore about 64 bytes per vector, roughly 3-4x the estimate the budget reason reports: at the boundary (16,777,216 vectors) the transient peak is close to 1 GiB while the message says 256 MiB. Consider raising _DUPLICATE_BYTES_PER_VECTOR to a peak-aware 64 so that both the gate and the reported estimate describe real usage.

# is visible and renderable instead of exposing zero-width buckets.
while True:
try:
counts, edges = np.histogram(array, bins=bin_count)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[suggestion] Each retry re-scans the whole array, allocates a discarded array.size index array and throws the counts away; the loop can iterate up to bin_count times and is reached with full-split arrays (norms, sparse nnz_per_vector). Whether the automatic edges are distinct depends only on min/max, because np.histogram(..., bins=k) derives linspace(min, max, k + 1); the largest usable bin count can be found by probing those candidate edges in O(bin_count) work, then counting once with explicit bins=edges.

Signed-off-by: DoubleCore <doublecore@users.noreply.github.com>
Assisted-by: OpenCode:ark-code-latest
@vsag-bot
vsag-bot self-requested a review November 5, 2026 09:08
@DoubleCore

Copy link
Copy Markdown
Author

Addressed both post-4a6ba2ac inline suggestions in e868d97, now pushed to this PR branch.

  • Exact duplicate detection now uses a peak-aware 64 bytes/vector budget covering the digest input, sorted copy, sort index, unique output, and counts. The gate, reported reason, regression assertion, and technical documentation are aligned.
  • Narrow nonconstant histograms now probe candidate linspace edges without repeatedly scanning the full input, then call np.histogram once with explicit bins=edges. The regression test verifies strictly increasing edges, retained counts, and exactly one full histogram call across the NumPy raise/duplicate-edge compatibility semantics.

Local validation (not a CI result):

  • Command: uv run --with pytest --with numpy --with scipy --with h5py python -m pytest tools/dataset_profiler -q
  • Result: 154 passed in 2.84s.

No C++ build was run for this Python-only dataset_profiler update.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated review finished with limited validation.
Review decision: Comment only. No approval was submitted.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (10625 changed lines across 34 files).
No publishable inline findings; validation remains incomplete.
Validation limited. This is not a clean bill of health.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

  • One or more planned validation steps failed or were not run.

Reviewed commit e868d97.

@vsag-lgtm vsag-lgtm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review @ e868d97f - 1 inline suggestion(s).

# np.unique additionally holds a sorted copy, sort index, unique output, and
# counts, bringing peak duplicate-detection state to about 64 bytes per vector.
_DUPLICATE_DIGEST_BYTES = 16
_DUPLICATE_BYTES_PER_VECTOR = 4 * _DUPLICATE_DIGEST_BYTES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[suggestion] Sync the user-facing profiler docs with the new duplicate-detection budget accounting.

This change budgets exact-duplicate detection at 64 bytes per vector (the retained 16-byte digest row plus np.unique's sorted copy, sort index, unique output, and counts), so duplicates.exact now silently becomes unavailable above roughly 4.19M vectors instead of ~16.7M - a user-visible reporting change. tools/dataset_profiler/TECHNICAL_REPORT.md was updated for this, but the user-facing pages were not: docs/docs/en/src/resources/dataset_profiler.md line 75 and docs/docs/zh/src/resources/dataset_profiler.md line 67 still say the exact-duplicate digests are "16 bytes per vector" with "a 256 MiB retained-state budget", which no longer matches the accounting introduced at this line. Please update both localized pages to document the 64-byte peak-aware accounting and the resulting availability threshold.

Signed-off-by: DoubleCore <doublecore@users.noreply.github.com>
Assisted-by: OpenCode:ark-code-latest
@vsag-bot
vsag-bot self-requested a review November 5, 2026 12:15
@DoubleCore

DoubleCore commented Nov 5, 2026 •

Copy link
Copy Markdown
Author

Addressed in 40c880ab:

  • Updated docs/docs/en/src/resources/dataset_profiler.md and docs/docs/zh/src/resources/dataset_profiler.md to distinguish the retained 16-byte digest from the 64-byte-per-vector peak-aware exact-duplicate budget, including np.unique's sorted copy, sort index, unique output, and counts under the 256 MiB budget.
  • Both localized pages now state the exact availability threshold of 4,194,304 vectors (256 × 1024 × 1024 / 64, approximately 4.19M). Above that threshold, exact-duplicate detection is marked unavailable while streaming and sampled metrics are still reported.

Validation: git diff --check passed. This is a documentation-only change, so Python tests were not rerun; there are no C++ changes.

@vsag-lgtm vsag-lgtm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review @ 40c880ab — docs-only increment (en/zh dataset_profiler.md); verified the new exact-duplicate budget wording against the implementation (_DUPLICATE_BYTES_PER_VECTOR = 64, shared 256 MiB full-split budget, 4,194,304-vector threshold, norms/nnz at 8 bytes per vector) — consistent, no inline findings.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewer | Code review

Automated pull request review completed.
Review decision: Approved. No actionable findings were identified in this complete static review. CI and merge requirements remain separate.

Model: gpt-6.1-sol; reasoning effort: high.
Review scope: high (10632 changed lines across 34 files).
No actionable inline findings were found.
Review coverage checks completed; this does not prove absence of defects.
Static review only; compilation and runtime tests are outside this review's scope.
File coverage: 34/34; observed passing test runs: 0.

Reviewed commit 40c880a.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Website and repository documentation 网站与仓库文档 area/tools Command-line, evaluation, and developer tools 命令行、评测与开发工具 kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 size/XXL 1000+ changed lines version/1.2 蚂蚁开源轻训营专属

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build a Dataset Feature Visualization Tool

5 participants