refactor(weekly-scan): filter the model list before sharding it - #269
Merged
Conversation
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
requested review from
BenjSz,
arielge,
kiszk and
kmehant
as code owners
August 2, 2026 11:22
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>
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
enabled auto-merge
August 6, 2026 07:49
BenjSz
disabled auto-merge
August 6, 2026 08:29
Update comments to clarify the purpose of the skip writer.
BenjSz
approved these changes
Aug 6, 2026
BenjSz
left a comment
Contributor
There was a problem hiding this comment.
LGTM - I edited one doctring.
Collaborator
assaftibm
enabled auto-merge
August 6, 2026 09:52
❌ spyre-test: failure |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
generate_weekly_shards.pychunks a downloads-ordered list into fixed-size shards (250 x1 / 100 x2 / 50 x4), andweekly_test.pythen 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-parallelare 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-fileis now required inweekly_test.py;--top-kand the fetch branches are gone. All three existing call sites already pass exactly--modeand--model-list-file, so no caller breaks.--modestays — it selects the batch size, the model class, the pipeline, and the table.New
--fetchflag onweekly_test.pyfor manual runs with no shard file: fetch by--mode, apply the sameprefilter_models, record the terminal rows, evaluate the survivors — one invocation. Mutually exclusive with--model-list-file, one required.--top-k/--max-paramsare 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;
--fetchruns the shared filter, so both entry points agree on what reaches a worker and the filter still has one implementation.generate-matrixgains 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_csvkeeps it on). Both weekly sinks passFalse: 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. Inweekly_test.pythe 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_batchkept a too-large check "as a defensive backstop for rows where parameters were unknown at fetch time." It doesn't — the onlyMAX_NUMBER_PARAMScomparison in the file was the parent's, andFAILURE_CATEGORY_MODEL_TOO_LARGEappears in_process_batchsolely in a traceback-suppression list. So an oversized model the fetcher couldn't size surfaces ascpu_load_failedor a worker timeout, exactly as before this change. Comments corrected rather than a backstop invented.result_sink.pywas string-duplicating"hardware_exception"with a comment explaining it dodged a circular import withweekly_test.py. The new leaf constants module removes the need, so the workaround is gone.Behaviour changes worth knowing
generate-matrixsucceeds — routine, givencancel-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/totalchanges 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 inweekly_test.py, rows written now always equals models handled.if: ... != '[]'guard rather than relying on how Actions treats a zero-lengthmatrix.include.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). Attests/root under--noconftest, followingtest_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 deletedadd_entryblocks produced, and aninspect.signatureguard on the positional-construction hazard atclickhouse_db.py:292.They also verify the import split: 18 pass with no
clickhouse_connectinstalled, 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 droppednot-implemented-adapter; shard JSON valid, nomodel_infoleak;matrix_x4=[]appeared naturally, which is what prompted theif:guards.weekly_test.py --fetch --top-k 12 --write-to-csv: wrote a fresh file, recorded 1not-implemented-adapterrow, evaluated 11 of 12, reportedProcessed 11/11. A second run against the same path was refused with an actionableFileExistsError.CsvResultSinkis now write-only — it refuses a non-empty existing file, reports nothing as blocking, and defaultsdedup_guardto 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-csvtherefore applies no skip window, and says so at runtime.--fetch).ruff,black,yamllintclean.Still needs the pod: one shard through
weekly_test.py. The batching path is untouched, but_process_batchruns in aspawnchild that re-imports the module, so a brokenfailure_categoriesimport 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" atpush-to-clickhouse.yaml:147-185, andtest_weekly_DEPRECATED.yamlfiring on the same cron slot as the real scan.🤖 Generated with Claude Code