Skip to content

feat(dispatch): analyze_with_dispatch_plan — stages 0+1.5 (#53) - #54

Closed
lilith wants to merge 9 commits into
mainfrom
feat/dispatch-plan
Closed

feat(dispatch): analyze_with_dispatch_plan — stages 0+1.5 (#53)#54
lilith wants to merge 9 commits into
mainfrom
feat/dispatch-plan

Conversation

@lilith

@lilith lilith commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

Adds analyze_with_dispatch_plan(slice, query, hints) + DispatchHints per the issue-#53 design. Layered on top of analyze_features — the existing entry is unchanged; codecs opt in by switching call sites.

This PR ships stages 0 and 1.5. Stage 2 is deferred until #47's corpus sweep validates the threshold values.

What lands

  • Stage 0 (free, runs in <1 µs):
    • Empty query.features() → return empty AnalysisResults immediately, no scan.
    • pixel_count ≤ 64 000 → bump the sampling budget to the pixel count (exhaustive).
    • pixel_count ≥ 8 000 000 → record an internal flag_extended_pass for the future Stage 2 retry. Not acted on in this PR.
  • Stage 1: Tier 1 + alpha + dimension + depth features run as today. The strict-grayscale classifier is now always run by the dispatch plan (its output is required for Stage 1.5; sub-µs cost on coloured content via the existing early-exit walk).
  • Stage 1.5 (post-Tier 1, pre-Tier 2/3):
    • is_grayscale = true → subtract CHROMA_DROP_FEATURES (Tier 1 chroma + all of Tier 2 + chroma DCT compressibility + UV noise/quant percentiles + skin-tone) from the remaining-tier query.
    • uniformity > 0.95 → subtract SATURATING_DROP_FEATURES (Laplacian percentiles + patch_fraction_fast + AqMap percentiles).
    • Dropped features come back as None from AnalysisResults::get — the layered defense in RawAnalysis::into_results guarantees no caller ever sees garbage.
  • Stage 2 is deferred. The flag_extended_pass field is recorded but never read; DispatchHints.target_zq / content_hash are accepted but never consumed. Both are reserved for the Stage 2 + result-cache follow-ups and can land additively.

Files

  • src/dispatch.rs (new): DispatchHints, DispatchPlan, dispatch::run.
  • src/lib.rs: re-exports DispatchHints, adds the documented analyze_with_dispatch_plan public entry.
  • src/feature.rs: adds two crate-internal const FeatureSets — CHROMA_DROP_FEATURES and SATURATING_DROP_FEATURES.
  • src/tests.rs: 6 new tests under tests::dispatch_plan.
  • CHANGELOG.md: [Unreleased] entry under "Added".

Stage 1.5 drop sets (final)

CHROMA_DROP_FEATURES (gated by is_grayscale = true):

  • Tier 1 chroma: ChromaComplexity, CbSharpness, CrSharpness, Colourfulness (experimental), SkinToneFraction (experimental).
  • Tier 2 (all 6 axes): Cb{Horiz,Vert,Peak}Sharpness, Cr{Horiz,Vert,Peak}Sharpness.
  • Tier 3 chroma (experimental): DctCompressibilityUV, NoiseFloorUV{,P25,P50,P75,P90}, QuantSurvivalUv{,P10,P25,P50,P75}.

SATURATING_DROP_FEATURES (gated by uniformity > 0.95, all experimental):

  • Laplacian percentiles: LaplacianVariance{P50,P75,P90,P99,Peak}.
  • PatchFractionFast.
  • AqMap percentiles: AqMap{P50,P75,P90,P95,P99} (mean / std kept — they're cheap byproducts of the same accumulator).

Test plan

  • cargo test --lib --features experimental,composites — 169/169 passing (6 new in tests::dispatch_plan).
  • cargo test --lib — 125/125 passing.
  • cargo clippy --lib --tests --features experimental,composites -- -D warnings — clean.
  • cargo clippy --lib --tests -- -D warnings — clean.
  • cargo fmt --check — clean.
  • Per-image cost reduction on 256² grayscale photo (issue-Dynamic dispatch tree: analyze_with_dispatch_plan API for adaptive per-image gating #53 acceptance: ≥40 % on chroma-heavy schemas like v2.1) — not measured here; can be validated downstream once a codec switches call sites.
  • v2.2-clean picker output unchanged when fed through both entry points — verified for the photo case in stage15_no_drops_on_typical_photo (bit-identical f32 values for every requested feature). On grayscale / flat inputs the dispatch-plan output legitimately differs from analyze_features because dropped features become None instead of the zeroed-default values the existing entry would emit; the issue spec calls this out as the expected semantics.

Cross-refs

@lilith lilith self-assigned this Apr 30, 2026
@lilith
lilith force-pushed the feat/dispatch-plan branch from 585d355 to 5c73277 Compare April 30, 2026 10:18
lilith added a commit that referenced this pull request Apr 30, 2026
…, multi-metric/RD-time guidance

Adds a 600-line cross-codec reference at zentrain/PRINCIPLES.md
covering what every picker / scorer consumer (zenjpeg, zenwebp,
zenavif, zenjxl, zenpng, zengif, zenpicker, zensim) must follow.

Sections:

  1. Crate map (one-line orientation per crate)
  2. What a picker is (the fn shape + bin contract)
  3. Data discipline:
     - sweep four dimensions
     - per-(image, size) zensim ceiling (#51)
     - sample-count floors → NaN dropout (#49)
     - pixel_budget train↔runtime match (#48)
     - OOD bounds in every bake
     - re-bake triggers
  4. Argmin objective (composing scores at codec edge):
     - default size-optimal recipe
     - hard time cap
     - soft RD-vs-time (μ bytes/ms)
     - time-to-percent-saved
     - multi-metric: one bake per metric vs single multi-output
       vs metric-as-input — recommendation A (one bake per metric)
     - RD-vs-time bake (#56) protocol
  5. Runtime contract (codec-side pseudocode template)
  6. Per-codec adoption notes:
     zenjpeg / zenwebp / zenavif / zenjxl (lossy + lossless) /
     zenpng / zengif / zenpicker / zensim
  7. Default settings (trainer flags, bake flags, sweep harness,
     runtime). Headlines: --activation leakyrelu, --dtype f16,
     align(16) wrapper, OOD always-on, schema-hash gate always-on.
  8. Validation gates (block release): 7-item checklist
  9. Known landmines: budget-mismatch, OOD-necessity,
     corpus-shift-needs-rebake, composite-features-stability,
     metadata-necessity, post-hoc-fix-impossible, metric-shipping.
 10. Cross-references + live tracking table of open work.

Cross-links:
  - zentrain/README.md docmap entry → PRINCIPLES.md
  - zentrain/FOR_NEW_CODECS.md preamble → 'read PRINCIPLES first'
  - top-level README.md companion-crates section → PRINCIPLES.md

Sourced from #1, #6, #9, #41, #43, #44 (PR), #45, #46, #47, #48,
#49, #50, #51, #52 (PR), #53, #54 (PR), #55, #56, #57 (PR).
Doctrine encoded matches the global rules in
~/work/claudehints/CLAUDE.md (sweep discipline, source-informing
benchmarks). Live tracking table at the doc tail.
lilith added 4 commits June 3, 2026 19:57
…dispatch tree (#53)

Adds public adaptive analyzer entry that:
- Skips all work for empty-feature requests (stage 0)
- Uses exhaustive budget for ≤64K-pixel images (stage 0)
- Drops chroma-tier features when is_grayscale=true (stage 1.5)
- Drops saturating Tier3 features when uniformity>0.95 (stage 1.5)
- Stores extended-pass flag for ≥8MP images for stage 2 (deferred)

New `analyze_with_dispatch_plan(slice, query, hints)` and
`DispatchHints` ship alongside the existing `analyze_features`,
which is unchanged. Internally the new entry runs Tier 1 + the
strict-grayscale classifier first, then narrows the remaining
query by subtracting two new crate-internal const FeatureSets
(`CHROMA_DROP_FEATURES`, `SATURATING_DROP_FEATURES`) before
dispatching Tier 2 / Tier 3 / palette. Dropped features come
back as `None` from `AnalysisResults::get` — the layered defense
in `RawAnalysis::into_results` guarantees no caller ever sees
garbage for a dropped feature.

`DispatchHints.target_zq` / `content_hash` are accepted but not
consumed in this PR; they're reserved for the corpus#47-gated
Stage 2 follow-up and a future content-hash result cache, both
of which can land without a public-signature change.

Tests cover each gate: empty-query short-circuit, grayscale
chroma-drop, photo no-drop parity vs analyze_features, tiny-image
exhaustive-budget smoke, ≥8MP smoke, flat-image dual-drop.

Closes part of #50 (sub-D); enables #46 stage 2 follow-up after
#47's corpus sweep validates the thresholds.
Returning None for a feature the caller asked for breaks any consumer
that fills a fixed-shape input vector — the picker MLP, regression
baselines, anything trained on a column-positional layout. The picker's
training distribution already includes grayscale and flat content; the
MLP knows what those features look like there. Dropping them puts the
consumer off-distribution.

Reframe the dispatch tree as purely additive:

- Remove `CHROMA_DROP_FEATURES` + `SATURATING_DROP_FEATURES` and the
  Stage 1.5 query-narrowing logic from `analyze_with_dispatch_plan`.
- Restore the canonical `IsGrayscale`-only gate for the strict-grayscale
  scan (matches `analyze_features` exactly — sub-µs early-exit on
  coloured content stays unchanged for picker callers that don't ask
  for the signal).
- Mirror the 16-arm const-bool dispatch from `analyze_features` so
  monomorphisation tables stay identical; only the Stage 0 budget
  overrides differ.
- Tests: drop the two Stage 1.5 drop tests; replace with a load-bearing
  parity test that asserts bit-identical numerics vs `analyze_features`
  for every `FeatureSet::SUPPORTED` member at 256×256 (above the
  exhaustive-budget threshold), plus a grayscale test that asserts
  every requested chroma feature is *populated* (not dropped).
- Update CHANGELOG + the public docstring to spell out the contract:
  for every requested feature, the dispatch plan returns the same
  `Some(_)` / `None` shape `analyze_features` would have returned.
- Expose `analyze_specialized_raw` as `pub(crate)` so dispatch can
  reuse the canonical specialiser.

Stages 2+ (extended-budget retry, selective Tier 3, derived
likelihoods) remain deferred. None of them will skip caller-requested
features either — the dispatch tree only ever adds compute, never
drops it.
…tive seat

target_zq and content_hash were speculative — neither stage 0 nor any
shipping stage today consumes them. Under the project's no-0.2.x rule,
shipping unused public fields commits forever to a shape based on
guesses.

Drop both fields. Keep DispatchHints as an empty #[non_exhaustive]
struct — the seat is enough. Future stages add fields additively
under 0.1.x without a public-signature change. Call sites that hand
&DispatchHints today (constructed via empty()/default()) keep
compiling unchanged when fields land.
Rebase-drift fix. Since this PR was authored, the canonical
`analyze_features` laplacian SIMD-pass gate widened from
`LaplacianVariance` alone to the full percentile set
(LaplacianVariance + P50/P75/P90/P99/Peak, #42/#49). The dispatch
plan's `tier1_wants_laplacian` helper still checked only
`LaplacianVariance`, so a caller requesting just a percentile
variant (e.g. LaplacianVarianceP90) via analyze_with_dispatch_plan
would skip the histogram pass and get zeros where analyze_features
populates the feature — violating the plan's own Some/None-parity
contract. Helper now mirrors the canonical builder exactly.
@lilith
lilith force-pushed the feat/dispatch-plan branch from 3705429 to ea70bf9 Compare June 4, 2026 02:01
@lilith

lilith commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

Rebased onto 0.2.0 (main @ 66d08f6) — green

Revived this PR by rebasing the 3 commits onto current main (the zenpredict 0.2.0 release-prep). New HEAD: ea70bf9. Final diff vs main is +523 / -1 across the same 4 files the PR always touched (CHANGELOG +28, src/dispatch.rs +247, src/lib.rs +70, src/tests.rs +179). Still purely additive — the only deletion is the analyze_specialized_raw visibility bump (fnpub(crate) fn) the dispatch module needs.

Conflicts resolved

  • CHANGELOG.md (×2, one per dispatch commit) — both in [Unreleased]. Kept main's new 0.2.0-era content (package-trim Changed + AnalysisResults::pack Added) and slotted the dispatch entry under [Unreleased]; the stage-1.5→stage-0 header/body rewrites from commits 2+3 applied on top, so the shipped entry reads (issue #53, stage 0) — the final purely-additive design.
  • src/tests.rs — both sides appended a new test module at EOF (main added the issue-Per-feature minimum sample-count floor for percentile features (NoiseFloor*P*, Laplacian*P*, etc.) #49 sample_count_floor module; this PR adds dispatch_plan). Kept both, reconstructed the module-close braces at the boundary.

Rebase-drift fix (one new commit, ea70bf9)

Since this PR was authored, the canonical analyze_features laplacian SIMD-pass gate widened from LaplacianVariance alone to the full percentile set (LaplacianVariance + P50/P75/P90/P99/Peak, from #42/#49). The dispatch plan's tier1_wants_laplacian helper still checked only LaplacianVariance, so a caller requesting just a percentile variant (e.g. LaplacianVarianceP90) via analyze_with_dispatch_plan would have skipped the histogram pass and returned zeros where analyze_features populates the feature — a violation of the plan's own Some/None-parity contract. The helper now mirrors the canonical builder exactly. All other gates (PAL_NEEDED_BY, TIER2_FEATURES, T3_NEEDED_BY, ALPHA_FEATURES, PALETTE_FULL_FEATURES, DEPTH_FEATURES, TIER1_FULL_FEATURES, TIER1_SKIN_FEATURES, DCT_NEEDED_BY, palette_wants_grayscale) verified to still match main 1:1.

Main's feature.rs fmt+clippy fixes are preserved untouched (git diff main..HEAD -- src/feature.rs is empty); the tests.rs change is purely additive (no removal lines).

Green status (all pass locally on ea70bf9)

gate result
cargo build ok
cargo test --lib 122 passed, 0 failed
cargo test --no-default-features --features experimental,hdr --lib 179 passed, 0 failed, 2 ignored (pre-existing perf tests, not from this PR)
cargo clippy --features experimental,hdr --all-targets -- -D warnings clean
cargo fmt --check clean
cargo build -p zenpicker ok

Stage 2 still correctly deferred to #47

The flag_extended_pass (≥ 8 MP) bit is recorded but never acted on — stage 2 (extended-budget retry on budget-sensitive features) remains gated on the #47 corpus sweep, exactly as designed. The shipped surface is stage 0 only: dimension-driven sampling-budget adjustment that never narrows the caller's feature query.

CI now runs against the rebased branch.

@lilith

lilith commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

Holding for completion rather than merging the stage-0 scaffold.

A stage-0-only merge (skip-empty-query / exhaustive-on-tiny / flag-large) is a stub. The value is the content-aware gating: stage 1.5 (drop chroma features on grayscale, drop saturating features on flat content) and stage 2 (extended-budget retry on large images). That can't merge safely until the gating thresholds — and which features are droppable — are validated against a corpus, i.e. blocked on #47: dropping a feature the picker actually needed would silently degrade picks.

Completion plan (tracked in #53):

  1. Build the Expand budget-research corpus: ≥50 screen-content + ≥50 mixed images for stable AUC #47 corpus (≥50 screen-content + ≥50 mixed).
  2. Sweep the gating thresholds (grayscale/uniformity cutoffs, the 8 MP large threshold, the droppable-feature sets).
  3. Re-add stages 1.5/2 with validated thresholds.
  4. Confirm no picker-accuracy regression.

This branch is rebased onto 0.2.0, green, and carries a real drift-fix (laplacian-percentile gate parity) @ ea70bf9 — revive from here. Converting to draft; blocked by #47.

lilith added 2 commits June 4, 2026 14:23
…ded pass (#53)

Layers Stage 1.5 + Stage 2 onto the Stage-0 dispatch tree. The
dispatcher now inlines the same tier passes analyze_specialized_raw
makes (against the random-access RowStream, so reordering tier1 /
strict-grayscale relative to tier2/3 changes no value), then:

- Stage 1.5: when the strict-grayscale classifier reports
  is_grayscale=true, subtract CHROMA_DROP_FEATURES (tier-1 chroma +
  all of tier-2 + chroma DCT compressibility + UV noise/quant
  percentiles + skin-tone). When uniformity > 0.95, subtract
  SATURATING_DROP_FEATURES (Laplacian percentiles + patch_fraction_fast
  + AqMap percentiles; AqMapMean/Std kept). Dropped features come back
  as None via into_results(narrowed, ...).
- Stage 2: on >= LARGE_THRESHOLD (8 MP) images, re-run the tier-3 DCT
  pass at 2x hf_max_blocks and overwrite only EXTENDED_PASS_FEATURES
  (patch_fraction_fast, aq_map_p99, noise_floor_y_p90) with the
  finer-sampled values.

Parity: every feature NOT dropped (1.5) and NOT one of the three
Stage-2 targets on a >=8 MP image is bit-identical to analyze_features
(same passes, same budgets, same gate booleans). The three Stage-2
features diverge by design — a strictly finer block sample is the point
of the retry.

Re-adds CHROMA_DROP_FEATURES / SATURATING_DROP_FEATURES to feature.rs
(verbatim names re-verified against current feature IDs) and adds
EXTENDED_PASS_FEATURES. tier1_wants_laplacian mirrors the current
all-percentile-variant gate (matches the post-rebase drift-fix).

Tests: grayscale drops chroma / colour keeps chroma (gate doesn't
misfire) / uniform drops saturating-but-keeps-mean-std / >=8 MP Stage 2
populates the three targets while every other tier-3 feature stays
bit-identical to the default-budget baseline. Existing parity test
retained (sub-8 MP, non-gray, non-uniform synth ⇒ no gate fires).
… uniformity gate (#54)

Adds examples/validate_dispatch_gates.rs — a picker-free gate-correctness
harness that runs analyze_features (full) AND analyze_with_dispatch_plan
(gated) on a stratified imazen-26 sample (50 screen + 100 photo/mixed,
seed=1, 147 scored), and for each gate measures the worst-case
full-analysis magnitude of every feature the gate WOULD drop on images
where its condition fired. Cross-checks the grayscale classifier against
a ground-truth per-pixel channel-spread measure (independent of the
analyzer) to detect misfires.

Findings (benchmarks/dispatch_gate_validation_2026-06-04.{tsv,meta}):

- GRAYSCALE gate: VALIDATED-SAFE. Fired 8/147 (5.4%), 0 misfires (never
  on an image carrying real chroma). Every dropped chroma feature is
  bit-exactly 0.0 on strict R==G==B grayscale. Shipped ENABLED.

- UNIFORMITY gate: UNSAFE on this corpus. uniformity>0.95 fires on
  text/line-art/document/diagram screen content that carries
  sparse-but-maximally-sharp edge signal — would drop
  laplacian_variance_peak=255, laplacian_variance_p99=38,
  patch_fraction_fast~0.99, aq_map_p99~5.9 (34 meaningful-drop events
  across 10 images). The issue-#53 saturation assumption holds for
  photographic flat content but not screen content — exactly the class
  the picker most needs to distinguish. Shipped DISABLED
  (ENABLE_UNIFORMITY_GATE=false); SATURATING_DROP_FEATURES retained for
  a content-aware-threshold follow-up.

Per HONEST-STOP: disabling an unsafe gate with the data behind it is the
intended outcome — the grayscale gate (the validated-safe win) still
ships. Tests updated: the uniformity test now asserts the disabled gate
yields pure parity with analyze_features.

Cargo.toml: new validate_dispatch_gates example (experimental+hdr).
CHANGELOG: stages 0+1.5+2 entry replaces the stage-0-only text.
@lilith

lilith commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

Stages 1.5 + 2 landed; gate-validated against imazen-26

Pushed 94086a4 (impl) + 3c6c2a0 (validation) onto feat/dispatch-plan (now at 3c6c2a0, rebased on 0.2.0).

What landed

  • Stage 1.5 — content-class gating. The dispatcher now inlines the same tier passes analyze_specialized_raw runs (against the random-access RowStream, so reordering tier1 / strict-grayscale relative to tier2/3 changes no value), then narrows the remaining query:
    • is_grayscale = true → drop CHROMA_DROP_FEATURES (tier-1 chroma + all of tier-2 + chroma DCT compressibility + UV noise/quant percentiles + skin-tone).
    • uniformity > 0.95 → drop SATURATING_DROP_FEATURES (Laplacian percentiles + patch_fraction_fast + AqMap percentiles). Shipped DISABLED — see validation below.
  • Stage 2 — extended-budget retry. On ≥ 8 MP images, re-run the tier-3 DCT pass at 2× hf_max_blocks and overwrite only EXTENDED_PASS_FEATURES (patch_fraction_fast, aq_map_p99, noise_floor_y_p90) with the finer-sampled values.
  • Parity contract. Every feature not dropped by Stage 1.5 and not one of the three Stage 2 targets on a ≥ 8 MP image matches analyze_features bit-for-bit. Dropped features return None, never a stale/zero Some(_). The three Stage 2 features diverge by design (strictly finer block sample).

Gate validation (imazen-26, picker-free gate-correctness)

Harness: examples/validate_dispatch_gates.rs. Stratified sample seed=1: 50 from screen/ + 100 across photo/mixed dirs, 147 scored (3 PNGs the image decoder rejected). For each gate, the worst-case full-analysis magnitude of every feature it would drop on images where its condition fired; grayscale cross-checked against a ground-truth per-pixel channel-spread measure independent of the classifier. Artifacts: benchmarks/dispatch_gate_validation_2026-06-04.{tsv,meta}.

Gate Condition fire rate Worst-case would-drop magnitude Meaningful (≥1.0) would-drop events Misfires Verdict
Grayscale (CHROMA_DROP_FEATURES) 8/147 (5.4%) 0.000000 (every chroma feature) 0 0 (never fired on real-chroma content) VALIDATED-SAFE → shipped ENABLED
Uniformity (SATURATING_DROP_FEATURES) 10/147 (6.8%) 255.0 (laplacian_variance_peak) 34 n/a UNSAFE → shipped DISABLED

Grayscale gate is safe: on strict R==G==B grayscale every dropped chroma feature is bit-exactly its default (chroma is definitionally zero), and the strict-equality classifier never fired on an image carrying real chroma.

Uniformity gate is unsafe as specced and is shipped OFF (ENABLE_UNIFORMITY_GATE = false). The #53 spec assumed uniformity > 0.95 ⇒ Laplacian percentiles pin to ~0 and patch_fraction_fast won't vary. That holds for photographic flat content but is violated on text / line-art / document / diagram screen content: a mostly-white page with sparse-but-maximally-sharp black text reports uniformity > 0.95 yet has laplacian_variance_peak = 255, laplacian_variance_p99 up to 38, patch_fraction_fast ≈ 0.99, aq_map_p99 up to 5.9 (34 meaningful would-drop events across 10 images — e.g. screen/1440x900/proofwiki__pythagoras). Dropping those discards the most discriminating line-art/text signals — exactly the class the picker most needs. SATURATING_DROP_FEATURES is retained for a content-aware-threshold follow-up (e.g. uniformity AND low-edge-peak).

Per HONEST-STOP, finding-and-disabling the unsafe gate is the intended outcome: the validated-safe grayscale win still ships.

Green gate (all pass, captured locally)

cargo build; cargo test --lib (123); cargo test --no-default-features --features "experimental,hdr" --lib (182, 2 pre-existing perf tests ignored); cargo clippy --features "experimental,hdr" --all-targets -- -D warnings; cargo fmt --check; cargo build -p zenpicker. Doctests pass too.

Still deferred

Kept as a draft: picker-level pick-agreement no-regression is not included here (only zenwebp's picker is in production). This PR validates gate correctness (no meaningful feature dropped where a gate fires), not downstream picker argmin stability. That no-regression sweep remains a follow-up.

… measure dispatch perf (#54)

TASK 1 — content-aware uniformity gate calibration (issue #53 follow-up):
Extended validate_dispatch_gates with tier1 edge_density/variance columns,
a content-aware gate condition (uniformity>0.95 AND edge_density<τ), and a
uniformity>0.95-subset class split (text/line-art vs flat-photo by a
ground-truth fingerprint that is NOT a gate input). Result on imazen-26:
NO safe tier1 threshold exists.
  - The uniformity>0.95 regime contains ZERO flat-photo images and 10/10
    document/text/line-art images. There is no safe population to gate on.
  - edge_density does not separate the classes: a near-blank page
    (wikipedia__einstein, edge_density=0.0, variance=7e-4) still carries
    patch_fraction_fast=0.994 — the AUC-0.880 screen discriminator. Every
    τ in 1e-4…5e-3 was UNSAFE (drops the picker's strongest signal).
  - Fixed the safety classifier: patch_fraction* is [0,1]-valued, so a flat
    MEANINGFUL_MAGNITUDE=1.0 mis-passed a maximal 0.99 drop; meaningful_
    threshold() now uses 0.27 (feature.rs screen-like operating threshold).
Honest-stop: ENABLE_UNIFORMITY_GATE stays false (grayscale-gate-only); the
dispatch.rs comment + validation .meta record the full calibration evidence.

TASK 2 — dispatch perf measurement (issue #50): new examples/dispatch_perf.rs
times analyze_features vs analyze_with_dispatch_plan (FeatureSet::SUPPORTED),
interleaved A/B (alternating which side runs first per rep, median over reps),
release / NO target-cpu=native. Finding: the gated path is net SLOWER —
parity (-0.66%) on <8 MP images, -37.85% on ≥8 MP images. The regression is
ENTIRELY Stage 2's extended Tier-3 DCT re-walk (deliberate accuracy tradeoff),
NOT the gating: the <8 MP neither-fired control is -0.66% (free). The grayscale
gate's win is not isolable here (all grayscale corpus images are ≥8 MP).
Stable at reps=25. Artifacts: benchmarks/dispatch_perf_2026-06-04.{tsv,meta}.

Green gate: cargo build / test --lib (123) / test --no-default-features
--features experimental,hdr --lib (182) / clippy -D warnings / fmt --check /
build -p zenpicker — all pass. No source behavior change (dispatch.rs is
comment-only; the gate remains correctly disabled).
@lilith
lilith marked this pull request as ready for review June 4, 2026 21:06
@lilith

lilith commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

PR #54 — content-aware uniformity gate calibration + dispatch perf measurement

Pushed d944258 to feat/dispatch-plan. Two tasks: calibrate a SAFE content-aware uniformity gate, and measure the dispatch gating's perf. Both done; both land as honest-stop / negative results backed by measured data. No source behavior change — src/dispatch.rs is comment-only, the gate stays correctly disabled.

Task 1 — content-aware uniformity gate: NO safe tier1 threshold exists on imazen-26

The gate fires post-tier1 / pre-tier3, so its condition may only use tier1 signals (always-computed: variance, edge_density, uniformity). The brief's hypothesis: text/line-art keeps high tier1 edge_density even when "uniform", flat-photo goes to ~0, so uniformity > 0.95 AND edge_density < τ would fire only on safe flat content.

Refuted on two counts (validate_dispatch_gates extended with edge_density/variance columns + a uniformity>0.95-subset class split, where "text-like" = laplacian_variance_peak >= 64 OR patch_fraction_fast >= 0.5 — a ground-truth fingerprint, NOT a gate input, so no circularity):

(1) The uniformity > 0.95 regime has no safe population to gate on. It contains 0 flat-photo and 10/10 document/text/line-art images:

subset n edge_density min / median / max variance min / median / max
TEXT/LINE-ART (unsafe to gate) 10 0.00000 / 0.00424 / 0.00940 0.001 / 78.6 / 2280.8
FLAT-PHOTO (safe to gate) 0

(2) edge_density does not separate the classes. The text-like set spans edge_density ∈ [0.0, 0.0094], including a near-blank page (screen/1920x1080/wikipedia__einstein, edge_density = 0.0, variance = 7e-4) whose single non-default signal is patch_fraction_fast = 0.994 — the documented AUC-0.880 screen-vs-photo discriminator (screens p50=0.726, photos p50=0.002). Any τ tight enough to fire on it still drops the picker's strongest feature.

Threshold sweep (--edge-density-max τ, meaningful-drop events on the fired set):

τ (edge_density_max) gate fired worst would-drop verdict
1e-4 1 patch_fraction_fast = 0.994 UNSAFE
5e-4 1 patch_fraction_fast = 0.994 UNSAFE
1e-3 4 laplacian_variance_peak = 255 UNSAFE
2e-3 4 laplacian_variance_peak = 255 UNSAFE
5e-3 7 laplacian_variance_peak = 255 UNSAFE

Every tested τ is unsafe. (Also fixed the harness safety line: patch_fraction* is [0,1]-valued, so a flat MEANINGFUL_MAGNITUDE = 1.0 mis-passed a maximal 0.99 drop as "safe"; meaningful_threshold() now uses 0.27 — feature.rs's screen-like operating threshold.)

Verdict: keep ENABLE_UNIFORMITY_GATE = false. SATURATING_DROP_FEATURES retained for a future revisit on a corpus with uniform photographic content. Full evidence in benchmarks/dispatch_gate_validation_2026-06-04.{tsv,meta}.

Re-validation table (counterfactual safety, imazen-26, n=147)

gate condition fire rate worst would-drop magnitude meaningful-drop events verdict
Grayscale is_grayscale 8/147 (5.4%) 0.000000 (every chroma feature) 0 VALIDATED-SAFE, ENABLED
Uniformity (naive) uniformity > 0.95 10/147 (6.8%) 255.0 (laplacian_variance_peak) 42 UNSAFE — correctly DISABLED
Uniformity (content-aware) uniformity > 0.95 AND edge_density < 5e-4 1/147 (0.7%) 0.994 (patch_fraction_fast) 1 UNSAFE — DISABLED

Task 2 — dispatch perf measurement (issue #50)

examples/dispatch_perf.rs times analyze_features (full) vs analyze_with_dispatch_plan (gated), FeatureSet::SUPPORTED, interleaved A/B (alternating which side runs first per rep, median over reps), release / NO target-cpu=native. Bench command:

cargo run --release --features "experimental,hdr" --example dispatch_perf -- \
  --corpus /home/lilith/work/codec-corpus/imazen-26 \
  --output benchmarks/dispatch_perf_2026-06-04.tsv \
  --screen-n 50 --photo-n 100 --seed 1 --reps 9 --content-unif-edge-max 0.0005

Headline: the gated path is net SLOWER (negative = gated slower than full). 7950X, n=147, reps=9 (stable at reps=25: −28.85% vs −29.50%).

slice n median full median gated mean savings
OVERALL (median 9.0 MP) 147 23.461 ms (2.251 ms/MP) 31.688 ms (2.980 ms/MP) −29.50%
< 8 MP 33 14.17 ms 14.28 ms −0.66% (parity)
≥ 8 MP 114 42.68 ms 58.11 ms −37.85%

Per-gate-fired subset (a gate only saves on images it fires on):

subset n mean savings note
grayscale-gate FIRED 8 −31.17% (median) all 8 are ≥8 MP — chroma-drop swamped by Stage 2; win not isolable here
content-unif WOULD-fire (disabled) 1 −24.81% the einstein page, 100 MP
neither fired 138 −29.47% dominated by the ≥8 MP Stage 2 cost
neither fired AND < 8 MP (pure-overhead control) 33 −0.66% the un-monomorphized dispatch structure itself is free

Root cause: the < 8 MP neither control is −0.66%, so the gating + multi-call structure is essentially free. The entire ~38% regression is Stage 2's extended Tier-3 DCT re-walk — on every ≥8 MP image, run_extended_pass re-runs the DCT pass at 2× hf_max_blocks to refine 3 features (patch_fraction_fast, aq_map_p99, noise_floor_y_p90). That's a deliberate accuracy-vs-speed tradeoff ("a strictly finer block sample is the whole point of the retry"), not a bug, but it costs ~38% wall-time on the common large-image case. Artifacts: benchmarks/dispatch_perf_2026-06-04.{tsv,meta}.

Final per-gate verdict

  • Grayscale gate → ENABLED. Bit-safe (0 misfires, every dropped chroma feature is bit-exactly its default on strict R==G==B). Unchanged.
  • Uniformity gate → DISABLED (honest-stop). No safe tier1 discriminator on imazen-26; the uniformity>0.95 regime is entirely document/screen content whose patch_fraction_fast is the picker's strongest signal and is uncorrelated with tier1 edge_density.
  • Stage 2 (extended pass) → the real perf cost. The gating is ~free; Stage 2 costs ~38% on ≥8 MP images. Whether the 3-feature accuracy refinement is worth that is a separate product call (Don't spend a millisecond more on analysis than the picker actually needs — umbrella #50) and should be decided on the accuracy-gain data, not assumed. Suggested next step: measure the Stage-2 accuracy gain (those 3 features, default vs 2× budget) on the same corpus, or gate Stage 2 behind an opt-in hint so analyze_with_dispatch_plan reaches parity with analyze_features by default.

Green gate (all pass)

cargo build ✓ · cargo test --lib ✓ (123) · cargo test --no-default-features --features "experimental,hdr" --lib ✓ (182, 2 pre-existing ignored) · cargo clippy --features "experimental,hdr" --all-targets -- -D warnings ✓ · cargo fmt --check ✓ · cargo build -p zenpicker

Pushed + verified on feat/dispatch-plan@origin (d944258). Not merged — leaving the merge to the orchestrator per instructions.

lilith added 2 commits June 5, 2026 21:21
)

Stage 2 (the 2x Tier 3 re-walk on >=8 MP images to refine the 3
budget-sensitive features) was a ~-38% wall-time regression on large
images, running unconditionally. Gate it behind a new
DispatchHints::enable_extended_pass (default false) + a
with_extended_pass() constructor. Default hints now skip Stage 2, so
analyze_with_dispatch_plan reaches numeric parity with analyze_features
on the default path (no large-image regression). Additive on the
#[non_exhaustive] struct.

Tests: default hints skip Stage 2 (parity); opt-in runs it and changes
at least one EXTENDED_PASS_FEATURE. 123 default + 184 experimental,hdr
lib tests pass; clippy + fmt clean.

Recovered from an agent run that was interrupted by a transient
server-side rate limit before commit.
Add examples/dispatch_perf_small.rs — downscales imazen-26 originals
(8 grayscale + 6 screen + 6 photo) to a log-spaced size set
{48..1024}px (Lanczos3, downscale-only), and times analyze_features vs
analyze_with_dispatch_plan(.., None) interleaved A/B (25 reps, median).

Measured (lilith, release, no target-cpu=native; benchmarks/
dispatch_perf_small_2026-06-05.{tsv,meta}):
- The grayscale gate's saving is PER-PIXEL, not fixed. On the grayscale
  subset (80 cells) gated beta drops 9.51 -> 8.44 ms/MP (~11% of per-pixel
  cost); fixed-overhead alpha delta ~0.006 ms (negligible). Per-size %
  saving holds ~6-11% across all sizes (16% at 48px, settling to ~9-11%
  by 768-1024px) and grows in absolute ms with size (0.006 -> 0.85 ms).
- On COLOR content (118 cells) the gate never fires: full==gated within
  noise (delta-alpha ~ -0.003 ms, delta-beta ~ +0.013 ms/MP). Confirms
  default-path parity / free dispatch on the dominant case.
- 123 lib tests pass incl. dispatch-plan parity; clippy/fmt clean.
@lilith

lilith commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

Small-image dispatch-plan perf sweep — settling whether the gating is worth shipping

Followup to the large-corpus run (which showed the gating is free at <8 MP but couldn't measure its win — that corpus is large-image-heavy, median 9 MP, so the saving was buried under per-pixel cost). New harness examples/dispatch_perf_small.rs downscales imazen-26 originals (the 8 grayscale + 6 screen + 6 photo) to a log-spaced size set and times both entry points interleaved A/B.

Setup: lilith (7950X), release, no target-cpu=native (runtime SIMD dispatch). Lanczos3 downscale-only (never upscale). 25 reps/cell, interleaved A/B (alternating first-side per rep), 3-rep warmup, median over reps. full = analyze_features(SUPPORTED); gated = analyze_with_dispatch_plan(SUPPORTED, None) (default hints → Stage 2 OFF, grayscale gate is the only enabled gate). Data: benchmarks/dispatch_perf_small_2026-06-05.{tsv,meta} @ 6a66178.

Per-size medians

ALL = grayscale + screen + photo mixed (n per size in parens). Grayscale-subset = the 8 grayscale images only (the gate fires on every one).

size (px) ALL full ms ALL gated ms ALL Δ% gray full ms gray gated ms gray Δ% gray ms saved
48 0.0367 0.0318 −0.5% 0.0382 0.0319 +16.4% +0.0063
64 0.0647 0.0575 −0.5% 0.0663 0.0577 +13.0% +0.0086
96 0.1377 0.1257 −0.4% 0.1384 0.1252 +9.6% +0.0132
128 0.2364 0.2187 −0.0% 0.2418 0.2217 +8.8% +0.0201
192 0.5287 0.4911 +1.3% 0.5385 0.5014 +6.9% +0.0371
256 0.9484 0.9012 +0.1% 0.9978 0.9337 +6.5% +0.0641
384 2.0685 1.9923 −0.1% 2.2004 2.0757 +5.4% +0.1247
512 2.4355 2.2756 −0.1% 2.4770 2.2549 +8.9% +0.2222
768 4.5817 4.2033 +0.3% 4.7031 4.2072 +10.5% +0.4959
1024 7.4057 6.6578 +0.9% 7.6604 6.8061 +11.0% +0.8543

(The ALL Δ% hovers around 0 because grayscale is only 8 of ~20 sources at each size and the gate never fires on the color majority. The signal lives in the grayscale subset.)

Linear fit total_ms = α + β·MP (OLS, all cells)

group (n) path α (fixed ms) β (ms/MP)
ALL (198) full +0.339 9.281
ALL (198) gated +0.342 8.747
GRAYSCALE (80) full +0.339 9.513
GRAYSCALE (80) gated +0.332 8.441
COLOR (118) full +0.341 9.058
COLOR (118) gated +0.344 9.045
  • Grayscale: Δα = +0.006 ms (negligible), Δβ = +1.07 ms/MP → the gate's saving is per-pixel, ~11% of per-pixel cost. It skips the chroma/Tier-2 passes, which scale with pixels.
  • Color: Δα = −0.003 ms, Δβ = +0.013 ms/MP — both indistinguishable from zero. Default-path parity holds: gating is genuinely free on the dominant (color) case, confirming the large-corpus result and Task-1's numeric-parity claim.

Verdict — there IS a real win, but it's NOT a small-image-only "fixed-saving" story

The original hypothesis was that the grayscale gate's saving is roughly fixed, so it'd only be a meaningful % on small images. The data says otherwise: the saving is proportional to pixels (Δβ ≈ 1.07 ms/MP on grayscale, Δα ≈ 0). So there is no crossover size below which it suddenly becomes worth it — the gate delivers a steady ~6–11% wall-time saving on strict-grayscale content at every measured size (16% at 48px is partly small-image timing-resolution noise; it settles to ~9–11% by 768–1024px and the absolute ms saved grows monotonically, 0.006 → 0.85 ms). On color content the gating is free.

So: ship the grayscale gate. It's free on color (parity-verified) and a clean ~6–11% win on grayscale at all sizes — no downside. The "worth it" question is really how much grayscale traffic the workload carries, not image size: a document/scan-heavy or line-art workload gets a real proportional speedup; a photo-heavy workload sees ~0 either way (but loses nothing). Note Stage 2 is opt-in/off by default and the uniformity gate is permanently disabled, so this PR's default behavior is exactly: grayscale gate + numeric parity on everything else. The measured grayscale win justifies keeping the gate enabled by default.

@lilith

lilith commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

Closing without merging — decision after the full validate-and-measure arc.

Built + validated (all green at feat/dispatch-plan @ 6a66178, preserved):

  • Stage 0 (free); Stage 1.5 grayscale gate (bit-safe — drops only bit-default chroma on true grayscale); Stage 2 made opt-in (default-off → default-path parity with analyze_features).
  • Uniformity gate: proven unsafe (fires on text/line-art; no tier1 discriminator separates it from flat-photo) → permanently disabled.
  • Perf: grayscale gate is a per-pixel ~6-11% wall-time win on strict-grayscale content at every size (Δβ≈1.07 ms/MP, Δα≈0), free on color. Stage 2 = ~-38% cost when opted in, for an unmeasured accuracy gain.

Why close, not merge: the only validated win (grayscale gating) is real but narrow — it only materializes on grayscale traffic — and carries costs the cost/benefit doesn't justify for current workloads:

  • Coupling/maintenance: drop-sets must stay in lockstep with the analyzer's tier/pass structure; drift silently breaks the parity contract (this already happened once — the laplacian-gate widening).
  • Silent failure mode: drop-set drift or an is_grayscale near-boundary misfire degrades picks with no error.
  • Stage 2 benefit unmeasured.

Branch 6a66178 preserved (impl + validation harnesses + benchmarks/dispatch_gate_validation_* + benchmarks/dispatch_perf{,_small}_*). Reopen/revive from there if grayscale traffic share grows enough to justify the coupling. Design tracked in #53.

@lilith

lilith commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

Preserved as a DO-NOT-MERGE exploration draft: #80 on branch exploration/dispatch-plan (same commit 6a66178). The original feat/dispatch-plan branch is being removed to consolidate — the work lives on exploration/dispatch-plan.

@lilith
lilith deleted the feat/dispatch-plan branch June 6, 2026 07:29
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.

1 participant