Skip to content

refactor(weekly-scan): filter the model list before sharding it - #269

Merged
assaftibm merged 25 commits into
mainfrom
refactor/weekly-scan-upstream-prefilter
Aug 6, 2026
Merged

refactor(weekly-scan): filter the model list before sharding it#269
assaftibm merged 25 commits into
mainfrom
refactor/weekly-scan-upstream-prefilter

Conversation

@assaftibm

@assaftibm assaftibm commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Problem

generate_weekly_shards.py chunks a downloads-ordered list into fixed-size shards (250 x1 / 100 x2 / 50 x4), and weekly_test.py then filtered within each shard. The filtered-out models cluster — config-class families cluster by download count — so surviving counts per shard varied wildly. Some scan jobs finished in minutes while others ran for hours against the same nominal shard size.

Fix

Filter before _chunk(), so shard size is a count of real evaluations.

Shard sizes, tier routing, the matrix shape and max-parallel are all unchanged — the only difference is what _chunk() receives. Expect fewer, fuller jobs, which also moves further from the 256-job matrix cap.

Filtering upstream means the filter has to be shared, so:

  • New model_prefilter.py — pure function, returns a three-way partition. The skip-window case is deliberately separate from the terminal categories: those models already have a recent row, so writing another would duplicate it or be silently swallowed by the sink's guard.

  • --model-list-file is now required in weekly_test.py; --top-k and the fetch branches are gone. All three existing call sites already pass exactly --mode and --model-list-file, so no caller breaks. --mode stays — it selects the batch size, the model class, the pipeline, and the table.

  • New --fetch flag on weekly_test.py for manual runs with no shard file: fetch by --mode, apply the same prefilter_models, record the terminal rows, evaluate the survivors — one invocation. Mutually exclusive with --model-list-file, one required. --top-k / --max-params are rejected without it rather than silently ignored.

    This is not a return to the unfiltered fetch path this PR removes. That one fetched without filtering, which is what made shards uneven; --fetch runs the shared filter, so both entry points agree on what reaches a worker and the filter still has one implementation.

  • generate-matrix gains ClickHouse access — it now reads the skip window and writes the terminal verdict rows itself.

  • add_entry's dedup guard becomes opt-out (dedup_guard, keyword-only, default True; import_csv keeps it on). Both weekly sinks pass False: the skip-window rule is evaluated once, upstream, so from then on a write records a decision already taken and a second check could only discard a row the caller chose to write. The skip-window rule is evaluated once, upstream, while the list is built; after that a write records the outcome of a decision already taken, so a second check could only discard a row the caller chose to write — leaving the run with fewer rows than the models it handled and no accounting for the gap. In weekly_test.py the guard would also have consulted a skip set snapshotted when that job started, two hours newer than the producer's, so it wasn't even re-asking the same question. A duplicate is the better failure: ReplacingMergeTree(snapshot_date) collapses same-day duplicates on merge.

Two things found along the way

The in-worker size backstop does not exist. A comment claimed _process_batch kept a too-large check "as a defensive backstop for rows where parameters were unknown at fetch time." It doesn't — the only MAX_NUMBER_PARAMS comparison in the file was the parent's, and FAILURE_CATEGORY_MODEL_TOO_LARGE appears in _process_batch solely in a traceback-suppression list. So an oversized model the fetcher couldn't size surfaces as cpu_load_failed or a worker timeout, exactly as before this change. Comments corrected rather than a backstop invented.

result_sink.py was string-duplicating "hardware_exception" with a comment explaining it dodged a circular import with weekly_test.py. The new leaf constants module removes the need, so the workaround is gone.

Behaviour changes worth knowing

  • Cancel safety (accepted). Terminal rows are written at fetch time, so their 10-day skip window starts then. Cancel a run after generate-matrix succeeds — routine, given cancel-in-progress: true — and those models read as "done" for 10 days though no scan ran. Mitigating: these are terminal properties of the checkpoint (a MoE model won't stop being MoE), so re-deriving them weekly is wasted work anyway.
  • Processed N/total changes meaning — from "fetched" to "to evaluate", so it now approaches N == total instead of being a small fraction. Confirmed in the integration run below. With the guard off in weekly_test.py, rows written now always equals models handled.
  • Empty tiers are now likely (a re-run inside the skip window), where they were near-impossible. Each scan job gained an if: ... != '[]' guard rather than relying on how Actions treats a zero-length matrix.include.
  • Skipped rows now land ~2h before scan rows, so a Saturday 22:00 UTC run can straddle midnight and split one logical run across two snapshot_dates. Not corrupting (ReplacingMergeTree), and pre-existing in kind — a 4320-minute scan job can already straddle days.

Verification

Unit tests — the first for any of these modules (tests/test_weekly_prefilter.py, 28 tests). At tests/ root under --noconftest, following test_adapter_coverage.py, because the root conftest imports torch. Covers each branch, precedence (window-skip wins, so no duplicate rows accumulate), input-order preservation, parameter coercion, the exact 14-column field mapping the deleted add_entry blocks produced, and an inspect.signature guard on the positional-construction hazard at clickhouse_db.py:292.

They also verify the import split: 18 pass with no clickhouse_connect installed, 10 sink-backed ones skip. That's what the leaf constants module buys.

Live against the HF API:

  • generate_weekly_shards.py --dry-run --top-k 30 --model-type embedding → 30 fetched, 27 kept, 3 dropped not-implemented-adapter; shard JSON valid, no model_info leak; matrix_x4=[] appeared naturally, which is what prompted the if: guards.
  • weekly_test.py --fetch --top-k 12 --write-to-csv: wrote a fresh file, recorded 1 not-implemented-adapter row, evaluated 11 of 12, reported Processed 11/11. A second run against the same path was refused with an actionable FileExistsError.
  • CsvResultSink is now write-only — it refuses a non-empty existing file, reports nothing as blocking, and defaults dedup_guard to False. So it is an output for no-DB runs, not a second skip-window implementation that behaved differently from ClickHouse for the same input. --fetch --write-to-csv therefore applies no skip window, and says so at runtime.
  • Argument contract checked across all 8 valid/invalid flag combinations (neither source, both sources, tuning flags without --fetch).

ruff, black, yamllint clean.

Still needs the pod: one shard through weekly_test.py. The batching path is untouched, but _process_batch runs in a spawn child that re-imports the module, so a broken failure_categories import would surface as a worker crash rather than a parent import error.

Acceptance criterion: on a full run, compare scan-job durations against last week's. Near-uniform durations is the point of the change.

Not in here

Dead get_all_models (result_sink.py:118/:284/:419, zero callers repo-wide) — separate commit. Also still outstanding: the "TEMPORARY debug step" at push-to-clickhouse.yaml:147-185, and test_weekly_DEPRECATED.yaml firing on the same cron slot as the real scan.

🤖 Generated with Claude Code

assaftibm and others added 7 commits August 2, 2026 13:37
The upcoming shared pre-filter needs MAX_NUMBER_PARAMS and the
FAILURE_CATEGORY_* strings, and it must be importable from
.github/scripts/generate_weekly_shards.py without dragging in a database
driver. Putting the constants in a module with no intra-repo imports and no DB
imports keeps the filter's import closure clean, so it (and its unit tests) can
run on a machine with no clickhouse_connect installed.

This also removes an existing workaround: result_sink.py string-duplicated
"hardware_exception" with a comment explaining it was avoiding a circular
import with weekly_test.py. It now imports the real constant.

Pure move, no behavioural change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
The weekly pipeline is moving its skip-window filtering upstream into the shard
producers, which means add_entry's automatic should_insert_row check becomes a
second evaluation of a decision already made. Left in place it can only reject a
row the producer deliberately chose to write — silently, with the rejection
visible nowhere except a per-row log line, while the run's final tally still
reports the full input count.

dedup_guard defaults to True so clickhouse_db.import_csv is unaffected: that
path has no upstream filter and derives its (inserted, skipped) return value
from the guard's verdict.

The flag is keyword-only and placed after `today` on all three constructors
because clickhouse_db.py constructs ClickHouseResultSink(mode) positionally; a
new positional parameter would misbind there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
Extracts the four pre-filter decisions from weekly_test.main() into a reusable
pure function, so the shard producers can apply them BEFORE chunking the model
list instead of each worker rediscovering them inside its own shard.

prefilter_models() returns a three-way partition rather than a simple
keep/drop. The skip-window case is deliberately separate from the terminal
categories: those models already have a recent row, so writing another would
either duplicate it or be silently swallowed by the sink's guard. Only the
terminal set goes to write_skipped_rows().

Two behaviours are preserved exactly, both easy to get wrong:
  - `is_supported is False` stays an identity comparison. A missing key or None
    means the fetcher could not determine the config class, which is not the
    same as knowing it is unsupported; `not row.get(...)` would silently drop
    every such row.
  - An unknown parameter count is not treated as zero or as oversized. Those
    rows pass through to the in-worker backstop that exists for them.

model_prefilter imports no database driver — the skip-window decision arrives
as an injected callable — so it and its tests run on an interpreter with no
clickhouse_connect. The tests verify that: 18 pass bare, 10 sink-backed ones
skip. Tests live at tests/ root and run under --noconftest, following
test_adapter_coverage.py, because the root conftest imports torch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
The sharder chunks a downloads-ordered list into fixed-size shards, and
weekly_test.py then filtered within each shard. Because the dropped models
cluster — config-class families cluster by download count — surviving counts per
shard varied wildly, so some scan jobs finished in minutes while others ran for
hours against the same nominal shard size.

Filtering before _chunk() makes shard size a count of real evaluations. Shard
sizes, tier routing, the matrix shape and max-parallel are all unchanged: the
only difference is what _chunk() receives. Expect fewer, fuller jobs, which also
moves further away from the 256-job matrix cap.

The sharder now writes the terminal verdict rows (not-implemented-adapter,
model_too_large, moe) itself, since it is the component that decides them. The
sink is opened per mode because it binds one table per instance, and with
dedup_guard=False because prefilter_models has already consulted the identical
skip set through should_insert_row.

Filter placement is deliberately after the model_info pop and before tier
routing: prefilter_models returns the same dict objects it was given, so a
surviving ModelInfo would break the shard JSON dump.

--dry-run skips the ClickHouse read and the writes, making the whole
fetch/filter/route/chunk path runnable without credentials. Verified against the
live HF API at --top-k 30 --model-type embedding: 30 fetched, 27 kept, 3 dropped
as not-implemented-adapter, shard JSON valid with no model_info leak.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
With filtering moved into the producers, weekly_test.py no longer decides which
models to run: --model-list-file becomes required and the --top-k fetch path is
removed, along with the four-branch pre-filter loop and its counters.

--mode stays — it selects the per-process batch size, the model class in
_load_on_cpu, the verification pipeline in eval_model, and the sink's table.

Nothing invoked the fetch path: all three call sites in push-to-clickhouse.yaml
already pass exactly --mode and --model-list-file, so requiring the flag breaks
no caller. Manual pod runs are served by the new prepare_weekly_model_list.py.

While removing the parent-side size check, found that the "defensive backstop
for rows where parameters were unknown at fetch time" that _process_batch was
documented as keeping does not exist — the only MAX_NUMBER_PARAMS comparison in
the file was the parent's. FAILURE_CATEGORY_MODEL_TOO_LARGE appears in
_process_batch solely in a traceback-suppression list. So an oversized model the
fetcher could not size surfaces as cpu_load_failed or a worker timeout rather
than model_too_large, exactly as it did before this change. Corrected the
comments in model_prefilter and its test that had repeated the false claim.

The sink here keeps dedup_guard at its default True: it costs one dict lookup
and is the last defence if a row for one of these models lands between the
producer's fetch and the scan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
weekly_test.py now requires --model-list-file, so the manual pod workflow needs
something to produce one. This is the single-run equivalent of the CI sharder:
fetch the top-K catalog, apply the same four pre-filters, record the terminal
verdicts, and write out what is left, printing the exact weekly_test.py command
to run next.

Three destinations, differing in how the skip window is sourced:
  - ClickHouse (default) — reads the skip set and writes terminal rows there,
    with dedup_guard=False since the filter already applied that rule.
  - --write-to-csv — the CSV is both the skip-window index and the destination,
    so dedup_guard stays ON: the file may hold rows from arbitrary earlier runs,
    and a re-run should behave like a DB re-run rather than append duplicates.
  - --no-db — no store at all; the skip window is NOT applied and verdicts are
    discarded, so it warns loudly that the output is unsuitable for a real scan.

Verified against the live HF API. CSV round-trip at --top-k 25: the first run
recorded 3 not-implemented-adapter rows, the second read the same file, placed
those 3 in the skip window, wrote 0 duplicates, and still kept the same 22
models to evaluate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
The shard generator now applies the pre-filters and writes the terminal verdict
rows, so it needs credentials of its own.

clickhouse-connect and python-dotenv are named explicitly in the install step
because `pip install -e .` does NOT pull them — they live in the `dev` and
`models-ops` dependency groups, which a plain editable install ignores. Omitting
them would fail the job at import time on the sink.

The connectivity check runs before the fetch rather than after: the fetch can
take 90+ minutes at top_k=10000, and bad credentials should not surface only at
the end of it. Adapted from the scan jobs' identical step, minus their cd/uv run
wrapper since this job uses plain python with working-directory '.'.

Added an `if:` guard against an empty matrix on each scan job. With filtering
now upstream, a tier can legitimately produce zero shards — a re-run inside the
10-day skip window is the ordinary case, and matrix_x4=[] already appeared in
local dry-run testing. Skipping explicitly beats depending on how Actions
happens to treat a zero-length matrix.include.

The header comment's rationale for the old design ("the skip-window dedup guard
needs to read ClickHouse live during the scan") is now inverted, so it is
rewritten, including a note on the cancel-safety tradeoff: terminal rows are
written at fetch time, so a run cancelled after generate-matrix leaves those
models marked done for 10 days. Also corrected the card arithmetic in the
tier-split comment, which said 10*1+4*2+1*4=22 while max-parallel has been 20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
assaftibm and others added 11 commits August 2, 2026 18:55
weekly_test.py was still constructing its sink with the dedup guard on, so
add_entry could silently decline a write when another run had recorded the same
model since the producer built the list. The run would then report fewer rows
than the models it processed, with the shortfall explained only by a per-row log
line — the same unexplainable-shortfall problem that motivated making the guard
optional in the first place.

The skip-window decision belongs to whoever builds the list. Once a model is in
the list, the run was asked to evaluate it, and the result is a fact about this
run that should be recorded. A duplicate is the better failure mode:
ReplacingMergeTree(snapshot_date) collapses same-day duplicates on merge anyway.

Worth noting the guard here would have consulted a skip set snapshotted when the
scan job started — newer than the producer's, two hours earlier — so it was not
even re-checking the same question the filter answered.

With the guard off add_entry always returns True, so the "guard rejected" branch
in the result loop is unreachable and is removed rather than left as dead code.
Verified with a CSV round-trip: guard on writes 1 row for a duplicate model,
guard off writes 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
prepare_weekly_model_list.py existed only because weekly_test.py could no longer
build its own model list, which left the manual workflow as two commands passing
a temp file between them. --fetch collapses that back into one invocation:
fetch by --mode, apply prefilter_models with --top-k and --max-params, record the
terminal rows, evaluate the survivors.

This is not a return to the fetch path removed earlier in this branch. That one
fetched WITHOUT filtering, which is what made shards uneven. --fetch runs the
same shared prefilter_models the CI producer runs, so both entry points agree on
what is worth handing to a Spyre worker, and the filter still has exactly one
implementation.

--fetch and --model-list-file are mutually exclusive and one is required, so a
run can never be ambiguous about where its list came from. --top-k and
--max-params are rejected without --fetch rather than silently ignored.

The two sink backends now differ in dedup_guard, deliberately:
  - ClickHouse: OFF, as before — every result is a fact about this run and the
    row count must match the models handled.
  - CSV: ON. The file is its own skip-window index, so re-running against it
    behaves like re-running against the database instead of appending a second
    copy of every row. Rows dropped that way are now logged per row, naming the
    reason, so the count never silently disagrees.

Verified against the live HF API: --fetch --top-k 15 --write-to-csv fetched 15,
recorded 2 not-implemented-adapter rows, evaluated 13; a second run against the
same CSV found all 15 in the skip window and wrote 0 duplicates. Argument
contract checked across all 8 valid/invalid flag combinations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
CsvResultSink was doing two jobs: destination for no-database runs, and
skip-window source for anyone who pointed a second run at the same file. The
second job is what forced it to keep the dedup guard on while ClickHouse ran with
it off — so the same input behaved differently by backend, and a CSV run's row
count could disagree with the models it evaluated.

CSV is now purely an output for no-DB test runs. It refuses a non-empty existing
file, so "the file is new" is enforced rather than assumed, and with no history to
read there is no dedup question: get_recent_blocking_entries is unconditionally
empty and dedup_guard defaults to False. Both sinks now write every row they are
handed, and the "guard rejected" branch in the result loop is dead again and gone.

One consequence is deliberately loud: --fetch --write-to-csv applies no skip
window, so it evaluates every fetched model that clears the other filters. main()
prints a NOTE saying so, since the alternative is a run that quietly re-tests
models scanned yesterday.

Nothing outside the tests depended on the read path — clickhouse_db.import_csv
reads its CSV with the csv module and writes through ClickHouseResultSink, so it
is unaffected and keeps the guard it relies on.

The dedup-guard tests were using CsvResultSink as a convenient concrete sink;
since it can no longer report anything as blocking, they now use a fake in-memory
sink, which tests the guard where it actually lives (the base class). Added tests
for the write-only contract. 33 pass with clickhouse_connect, 18 pass and 15 skip
without it.

Verified live: --fetch --top-k 12 --write-to-csv wrote a fresh file and evaluated
11 of 12; a second run against the same path was refused with an actionable error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
No behavioural change — this only rearranges the previous commits so the diff
against main reads as "one deletion, one addition" instead of interleaved edits.

Three sources of avoidable churn, all from the new logic having been threaded
through main() rather than kept beside it:

  - adapter_dates and the sink block had been moved, turning a pure deletion into
    a move-plus-delete that git renders as alternating +/- lines. Both are back on
    their original lines, so the sink block now shows only the one dedup_guard
    argument and one changed string.
  - total/processed/overall_start had drifted below the list resolution for no
    functional reason; restored.
  - a 14-line rationale comment had landed mid-block, splitting an otherwise
    untouched region. It moves to a "Result rows" section in the module docstring,
    with a three-line pointer at the code.

The fetch/load branch is now _resolve_model_list(), so the entire new code path
sits outside main() as pure addition and main()'s remaining diff is the ~130-line
prefilter deletion plus two call-site lines.

Verified unchanged: --fetch --top-k 10 --write-to-csv behaves identically
(10 fetched, 1 terminal row, 9 evaluated, Processed 9/9), the argument contract
still rejects --top-k without --fetch, and 33 unit tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
No behavioural change. Removes churn that made the diff larger than the change,
and deletes code the CSV rewrite had orphaned.

result_sink.py:
  - Deletes _coerce_snapshot, _SNAPSHOT_DATE_FORMATS and _within_skip_window.
    The CSV read path was their only caller (ClickHouse filters by date in SQL),
    so they were left unreachable by the write-only rewrite — 22 lines that ruff
    does not flag and a reviewer would have to check by hand.
  - Restores the original CSV-then-ClickHouse ordering in the module docstring,
    so only the one bullet that actually changed shows as changed.
  - Cuts the dedup_guard rationale down to one place. It had been repeated across
    the module docstring, ResultSink.__init__ and the CsvResultSink class
    docstring; __init__ now keeps only the pointer plus the positional-parameter
    warning, which is the part a caller must not get wrong.

generate_weekly_shards.py: _prefilter_for_mode's docstring no longer restates the
module docstring it sits under.

push-to-clickhouse.yaml:
  - Un-reflows two header paragraphs that were rewrapped for line width without
    any wording change, so the diff shows only the rationale that is genuinely
    obsolete.
  - Reverts the "10*1 + 4*2 + 1*4 = 22 cards" comment to its original text. The
    arithmetic disagrees with max-parallel: 20, but that predates this branch and
    correcting it here just raises an unrelated question mid-review.

Verified: 33 tests pass, YAML parses with all three if-guards intact,
black/ruff/yamllint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
get_recent_blocking_entries returned list[dict] but no caller ever read a dict —
should_insert_row's `return not self.get_recent_blocking_entries(...)` was the
only consumer, and it just tested emptiness. The ClickHouse implementation
admitted as much in its own docstring, fabricating [{"model_name": key}] as a
"lightweight placeholder" purely to be truthy.

So the list was a boolean in a costume. should_insert_row becomes the abstract
method instead: ClickHouse is now `key not in self._skip_model_names`, and CSV is
`return True` rather than an empty list that has to be explained.

Also deletes get_all_models (abstract + 2 implementations, ~30 lines). It has no
callers anywhere in the repo — the ClickHouse version even carried an
argMax(...) GROUP BY query that nothing ever ran.

Note this makes the diff for result_sink.py larger (158 -> 221 changed lines)
while the file itself shrinks from 499 to 424, because removal reads as
deletions. Deliberate: deletions are the cheapest lines to review — you check
that the code is unreachable rather than that new logic is correct — and the
class ends up with two fewer abstract methods for implementers to satisfy.

Verified: 33 tests pass, and a live --fetch --top-k 8 run behaves identically
(8 fetched, 1 terminal row, 7 evaluated, Processed 7/7).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
dedup_guard existed for exactly one caller: clickhouse_db's --add_csv backfill,
whose (inserted, skipped) return was derived from the guard's verdicts. That CLI
turns out to be dead — nothing in the repo, any workflow, or any doc invokes
clickhouse_db.py as a script, and import_csv/insert_model_row have no callers at
all (ClickHouseResultSink writes via bulk client.insert, not insert_model_row).

So the parameter was supporting machinery for something nobody runs, while every
live caller passed False. Removed:

  - clickhouse_db.py: import_csv, insert_model_row, print_table, the three
    _parse_* helpers and the __main__ block (--add_csv / --drop). 314 -> 116
    lines. The schema constants, DDL, get_client and table_exists stay — those
    are what result_sink imports, and they are the live part of the module.
  - result_sink.py: the dedup_guard parameter from all three constructors, and
    add_entry's conditional. add_entry now always writes and returns None rather
    than a bool nothing branched on any more. 499 -> 397 lines.
  - The three weekly call sites lose their explicit dedup_guard=False, and
    skip_writer loses the branch that handled a rejection that can no longer
    happen.

Note --drop was a manual DB-surgery utility. Unreferenced, and recoverable from
git history if anyone wants it back.

The guard tests become TestAddEntryAlwaysWrites, asserting the contract that
matters now: a row is written even when should_insert_row reports the model as
blocked, repeated writes both land, and should_insert_row still reports blocks
for the producers that call it directly.

Verified: 33 tests pass (18 + 15 skipped with no DB driver), and a live
--fetch --top-k 8 run wrote exactly 8 rows for 8 fetched models
(1 terminal + 7 evaluated).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
…n -> --write-to-csv

Three naming/shape fixes, no change to what gets filtered.

prefilter_models(rows=...) -> prefilter_models(models=...). "rows" came from the
catalog/CSV vocabulary, but at this call site they are models fetched from the
HuggingFace Hub — and worse, "row" already means the *output* DB row elsewhere in
the same module (SkippedModel.row, "produce a row recording that verdict"). The
rename disambiguates both.

should_scan -> is_due_for_scan, with a documented IsDueForScan type alias. The
bare Callable[[str], bool] was opaque at the call site; the alias says what the
bit means and points at sink.should_insert_row.

The parameter stays a callable rather than becoming the sink itself, deliberately:
annotating a sink would make model_prefilter import result_sink -> clickhouse_db ->
clickhouse_connect, and the DB-free import is what lets this module and 18 of its
33 tests run on a host with no driver installed. The alias's docstring records
that reasoning so the next reader does not have to rediscover it.

The sharder's --dry-run becomes --write-to-csv, mirroring weekly_test.py. It was
the only caller with no sink, which is what forced the `lambda _: True` stub; now
it builds a CsvResultSink (one file per mode, suffixed -generative/-embedding) and
every caller passes sink.should_insert_row. Behaviour is unchanged — that sink is
write-only, so it reports nothing as already-scanned — but the mechanism is now
honest rather than a bypass, and the verdicts land somewhere inspectable instead
of being discarded.

Verified live: --write-to-csv --top-k 20 --model-type embedding fetched 20, wrote
2 not-implemented-adapter rows to verdicts-embedding.csv, sharded the remaining 18,
and needed no credentials. 33 tests pass; model_prefilter still imports with no
clickhouse_connect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
The docstring justified it as "so this module needs no database imports", which
does not survive scrutiny: a TYPE_CHECKING-only Protocol import would satisfy
that too, and generate_weekly_shards already uses exactly that trick for its own
ResultSink annotation.

The reasons that do hold: one bit per model is the entire dependency, and a sink
parameter would additionally type this module as able to call
add_entry/flush/close — none of which the filter should touch. The DB-free import
is a real benefit, just not a sufficient argument on its own, so it is now listed
second rather than as the justification.

Comment only; no code change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
…ema leaf

The manual refactor's structure holds up — the sink package split, the ModelType
enum, and extracting the worker into weekly_sub_process all make the flow easier
to follow. But it introduced three defects that would have broken a scheduled
run, and left the docs describing the pre-refactor world.

ModelType formatted as its qualified name, not its value. `class ModelType(str,
Enum)` inherits Enum.__str__, so every f-string interpolation rendered
"ModelType.GENERATIVE". Shard files were being written as
ModelType.GENERATIVE-x1-shard-000.json and every operator-facing log line was
affected. Now StrEnum, whose __str__ is str.__str__.

weekly_test.py --fetch crashed on its first result. _prefilter_for_mode did
`with sink:`, closing a sink it did not own; main() then kept writing to it and
hit "ValueError: I/O operation on closed file". The helper no longer closes a
sink it was handed, and the ownership rule is documented on the ABC. Also moved
main()'s try to open before the model list is built, so a Hub outage still
closes the sink, and pre-bound the five names the finally block reads — `total`
was previously bound only inside the try, so a fetch failure raised
UnboundLocalError from the cleanup path and masked the real error.

--write-to-csv reached for ClickHouse. sink_factory imported ClickHouseResultSink
at module scope, so the CSV branch constructed a client and read .env. Deferred
again.

Chasing that last one surfaced a pre-existing flaw the old comments papered
over: csv_sink needs one pure tuple (TABLE_COLUMNS) but took it from
clickhouse_db, which imports clickhouse_connect and calls load_dotenv() at module
scope. The flag never worked driver-free despite comments claiming it did. Split
the table shape into a dependency-free table_schema leaf; clickhouse_db keeps the
client and re-exports the schema, so existing importers are unaffected. The DDL
and TABLE_COLUMNS stay in the same file because they must agree — a positional
bulk insert means drift writes values into the wrong columns rather than raising.

Also fixed: generate_shards never closed its sink (leaking the handle and never
flushing ClickHouse's buffered verdict rows); asyncio.Queue annotated where a
multiprocessing.SimpleQueue is passed; a sys.path.insert that ran after the
imports it was meant to enable, working only because CI happens to run from the
repo root; the x1_shardd_sizes typo; a closing log line naming results.csv when
results-generative.csv was written (both now derive the name from one helper).

Tests: 33 -> 70. Every prefilter_models call needed rewriting for the new
signature (is_due_for_scan -> sink, max_params now required), and the sinks moved
out of result_sink. Added coverage for what the refactor introduced — ModelType
formatting, fetch_and_filter's ownership contract, create_sink's branching — plus
test_table_schema.py, which asserts the DDL and TABLE_COLUMNS agree in order
rather than leaving it to the three comments that used to warn about it. Each new
regression test was verified by reverting its fix and confirming it fails; one
early attempt passed either way (sys.modules caching made in-process import
blocking a no-op) and was replaced with a subprocess check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
@assaftibm
assaftibm requested a review from anubhavjana as a code owner August 4, 2026 00:42
assaftibm and others added 5 commits August 4, 2026 16:13
Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>
Removing the skip window left create_sink with a required recovery_run
parameter that no caller passed, so both producers -- weekly_test.main and
generate_weekly_shards -- raised TypeError on every run. Drop the parameter;
ClickHouseResultSink.fetch_hw_failure_models stays as groundwork until a
recovery run is actually exposed upstream. snapshot_date goes too: it became
dead once CsvResultSink stopped taking today=.

Two smaller breaks from the same commit: an unbalanced paren in
prefilter's summary line, and a timedelta import left unused when
_fetch_blocking_names went.

Tests: _StubSink existed only to answer should_insert_row, so the pre-filter
tests now construct no sink at all -- which is what the split was for. Tests
asserting removed behaviour are replaced by ones pinning the new contract,
including guards that prefilter_models takes no sink and the ABC exposes no
filtering hook.

Docs: two comments were actively wrong rather than merely stale.
HardwareExceptionAbortError claimed aborted rows are picked up automatically
via the sink's retry-on-hardware_exception rule; nothing does that now. The
workflow's empty-matrix guard was justified by "a re-run inside the 10-day
skip window" -- still needed, but because a tier can have no models in its
parameter range or be skipped by --model-type.

Signed-off-by: Assaf Toledo <assaf.toledo@ibm.com>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Instrument the six phases of build_catalog — Hub fetch, filter_fn, config
class lookup, row assembly, CSV write, and model_info/is_moe attach — and
print a breakdown with each phase's share of wall clock before returning.

Makes it visible which part of a large catalog run dominates; the two
threaded phases report pool wall-clock rather than summed CPU time, which
is the right number for "how long did this step take".

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

# Conflicts:
#	tests/spyre/weekly_generation/weekly_test.py
An over-indented `write_to_csv=base` argument in TestSinkFactory failed
the pinned black 26.3.1 pre-commit hook. Introduced in 44fb7f7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@BenjSz
BenjSz enabled auto-merge August 6, 2026 07:49
@BenjSz
BenjSz disabled auto-merge August 6, 2026 08:29
Update comments to clarify the purpose of the skip writer.

@BenjSz BenjSz left a comment

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.

LGTM - I edited one doctring.

@ashokponkumar

Copy link
Copy Markdown
Collaborator

@HarikrishnanBalagopal ptal

@assaftibm
assaftibm enabled auto-merge August 6, 2026 09:52
@assaftibm
assaftibm added this pull request to the merge queue Aug 6, 2026
@spyre-ci

spyre-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

❌ spyre-test: failure

:warning: orch *trigger-pr-validation* — *yellow* · arches amd64 · fp amd64=a8f0ea18
L0 deeptools                amd64 ❌
L1 flex                     amd64 ⛔
L2 aiu-toolbox/ibm-aiu-toolbox-e2e amd64 ⛔
L2 spyre-comms              amd64 ⛔
L3 spyre-backend/spyre-backend-dev amd64 ⛔
L4 torch-spyre/torch-spyre-dev amd64 ⛔
L5 hf-adapters/hf-adapters-dev amd64 ⛔

Merged via the queue into main with commit ac32f92 Aug 6, 2026
78 of 79 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.

3 participants