Skip to content

feature: per-phase timing and cache skip stats in get_job / job events #867

Closed
2 of 2 issues completed
Closed

Description

@dkackman

Problem

To answer "where does the time go and did the cache do anything", the matrix had to write tools/phases.py, which reconstructs load / encode / denoise / decode minutes and seconds-per-pass by walking /api/jobs/<id>/event-log. And there is no way to see whether a cache skipped anything:

  • P2 (first_block 0.05 at 12 steps) took exactly as long as its baseline - the only evidence it skipped nothing was s/pass (56.3 vs 55.8). The event log has quantized_cache hit/miss events for weights, but nothing for the step cache.
  • P1 (first_block at 30 steps) averaged 42.2 s/pass vs 55.8 - inferred ~25% skipped, never reported.
  • Yet P2's output differed from B3 (PSNR 25.6 dB, where a plain rerun P4 was 44 dB), so the hook changes numerics even when it saves nothing - only visible by diffing outputs.

Proposal

  1. get_job (and the MCP get_job) returns a timing block per generation step: load_s, encode_s, denoise_s, passes, s_per_pass (first and rest - the first pass carries compile/autotune), decode_s, save_s.
  2. A cache_stats event at the end of denoise: cache type, threshold, per-block or per-step computed vs skipped counts (diffusers' hooks hold this state; read it before reset). Surface as timing.cache = {type, skipped_passes_equiv, hit_rate}.
  3. Same for compile: compile_s on the first pass.

This also gives the plan estimator (separate issue) clean inputs.

Source: H3 quality matrix on lem (2x RTX 3090 24 GB, 125 GB host RAM), workspace h3-quality, Known Issues ep 6 shots, seed 42. Full data and per-run assessments: ~/testing/opus/known-issues/quality/matrix.json, report quality/REPORT.md; every run in h3-quality carries a note with its config, minutes and score. Related: #861 (follow-up experiments).

Activity

  1. added
    performanceTiming/perf regression flagged by the regression agent
    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)
    owner:leadFeature lead's turn (harnest R11)
    on Oct 14, 2026
  2. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Plan v2: per-step timing and step-cache skip counts in get_job

    Lead: claude-opus-5-5 (anthropic). Design session checked against develop @ 61838a9; v2 written in the decompose session against develop @ f8fda59. v2 adopts Don's review (manifest-entry carrier, real-pipeline cache-context test) and records his answers to Q1–Q4.

    Verdict: build smaller

    Axis Finding
    Value Don's own driving sessions gain this, on every speed or quality experiment. The H3 matrix (h3-quality, 63 jobs, Don's workspace) needed a reconstruction script to get phase minutes and s/pass. Whether P2's cache skipped anything was inferred from s/pass. #875 (first_block threshold sweep, MagCache) asks "how many passes did it skip, at what quality"; #866 (the estimator is off by up to 12x) wants measured load + passes × s/pass + decode inputs. Related closed class: #262 and #95 (phase durations argued from event diffs), #357 (phases misread).
    Build cost 2 stages, ~$3–5 + $3–5 = **$6–10** (v1: $7–11; the manifest carrier drops the history column and the Job fold).
    Inertia One optional timing key on each ManifestEntry (already extra="allow", already returned by get_job, stored in history and written to the run's manifest.json), declared sometimes(...). One new event type, cache_stats. A regenerated OpenAPI contract. About 30–50 tokens of MCP description against a surface budget with 0.5 tokens of headroom (tests/test_mcp_server.py:1594), so the budget is raised with a dated comment. A coupling to private diffusers hook state (FBCSharedBlockState.should_compute, MagCacheState), which a diffusers upgrade can move. No security boundary moves. No new workflow syntax.
    Reversibility Good. timing is additive and sometimes(...) (absent on runs before it, on reused steps, on steps with no entry of their own), so dropping a sub-field later is a doc change, not a breaking-change. Nothing in workflow JSON names it.
    Cheaper alternatives (a) Keep phases.py. Can't answer the cache question (no hook counts skips) and loses load/encode on long jobs (200-event history tail). (b) Only cache_stats. Answers #875, not #866. (c) Wall-clock from server event at. Too coarse for s/pass, and the 200-event trim still applies.
    Overlap #866 is a consumer, not a dependency; this plan doesn't wire timing into dw/plan.py or observed_cost.py. #868 would cite hit_rate but needs nothing from here. #865 is independent. #875's carved fix (MagCache calibration ratios) edits the same stateful_cache_context (components.py:833-864) as stage 2: whichever lands second rebases onto the other; not a blocker.

    Cut from the proposal, and what would bring each back:

    • compile_s as its own field. Compile is lazy and lands inside the first denoise forward; nothing in dw can separate it. Instead first_pass_s and pass_s (median of the rest) with compiled: true|false; the docs say compile ≈ first_pass_s − pass_s on a cold run. Brought back by: a diffusers/torch hook that reports compile time directly.
    • skipped_passes_equiv. Same number as skipped in other units.
    • Skip counts for taylorseer, faster, text_kv. Their hooks keep no decision state. They report type and config with null counts (Don, Q3). Brought back by: a matrix run that needs them; for taylorseer, wrapping _measure_should_compute.

    Problem

    Two questions have no answer from dw today: where did a generation step's time go, and did the step cache skip anything. The first is reconstructable from events only by a script, and only while the 200-event history tail still holds load and encode. The second is not reconstructable at all: diffusers' cache hooks keep only the latest decision.

    Non-goals: pricing from measured timings (#866), per-block cache stats, timing for utility-task steps beyond save_s, UI display, timing on the SSE stream beyond the existing events, timing on wait_for_job's slim reply (Don, Q1).

    What the sweep found (corrections to the proposal)

    1. There is no encode phase. PHASES is loading, cached, generating, decoding, saving, task (dw/events.py:219). generating fires before self.pipeline(...) (pipeline.py:635), so "generating → first pipeline_step" is encode plus the first pass, plus compile. The docs call that interval "encode" (docs/SERVER.md:210, docs/MCP.md:403), wrong whenever pass 1 is long. A real encode_s needs a denoise-start mark: the first denoiser forward (a pre-forward hook) for callback pipelines; for modular pipelines (H3, LTX-2, Qwen-Image) the opening of the denoise block's tqdm bar, which reported_progress_bars already patches (progress.py:88-130).
    2. Diffusers counts no skips. first_block_cache.py:51-62 keeps only should_compute; mag_cache.py:140-168 resets accumulated_steps on every compute. dw counts them itself, per forward, not per step (CFG runs more than one forward per step), read before stateful_cache_context's finally calls _reset_stateful_cache() (components.py:833-864).
    3. The event log can't be the source. History trims to 200 events (job_record.py:53). Timing is measured in the worker with a monotonic clock and recorded on the step's manifest entry.
    4. ManifestEntry is open (extra="allow") but undeclared keys are undocumented. timing is declared sometimes(StepTiming) on it (api_models.py:381), and the OpenAPI contract regenerated (scripts/dump_openapi.py, then npm run gen:api; tests/test_api_contract.py:141 and CI fail on drift). JobDetail is unchanged.
    5. Step identity comes free. The entry is written per step in record_step (workflow_run.py:886) with step, reused and, for rolled-up child entries, parent_step; extra keys reach manifest.json unchanged (write_run_manifest, workflow_run.py:217). A chain (chain.py:460,525) restarts the denoise counter per segment.

    The design

    timing on each manifest entry (REST GET /api/jobs/{id} manifest[], MCP get_job, run manifest.json)

    A pipeline step's manifest entry gains timing:

    {"step": "shot", "files": ["..."], "subfolder": "final",
     "timing": {"load_s": 41.8, "load_cached": false,
                "encode_s": 54.2,
                "denoise_s": 1689.0, "passes": 30,
                "first_pass_s": 61.0, "pass_s": 56.1, "compiled": false,
                "decode_s": 52.7, "save_s": 9.4,
                "segments": 1,
                "cache": {"type": "first_block", "threshold": 0.05,
                          "forwards": 60, "computed": 44, "skipped": 16, "hit_rate": 0.267}}}
    • All _s values are worker monotonic seconds, rounded to 0.1.
    • load_s is the step's component load. load_cached: true marks a pipeline served from the worker's model cache: its load_s is the lookup, not a load.
    • encode_s: pipeline call → first denoiser forward (or the modular denoise bar opening). denoise_s: from there to the last step. decode_s: last step → pipeline return. save_s: the result save, which already times itself (result.py:444-493).
    • pass_s is the median of passes 2..n; null when passes < 2.
    • Chain (Don, Q2): one summed timing per chain step: segment times summed, passes summed, segments: N, pass_s the median over every non-first pass in every segment.
    • A field with no mark is null, never guessed. A pipeline with neither a step callback nor a tqdm bar has denoise_s but null encode_s, passes, pass_s.
    • Reused step: the entry already carries reused: true; it gets no timing key (nothing ran).
    • No entry, no timing: a step whose entry the parent writes (parent_saves_this, A sub-workflow step saves its output twice and the manifest carries two entries under the child's step name, so a composed run doubles storage and shadows a step key #92) has no entry of its own, so no timing. Accepted cost of the carrier (Don's review). A utility-task step's entry never carries pipeline fields (at most save_s).
    • cache is present only when the step set a cache (stage 2). For taylorseer, faster, text_kv: {type, threshold: null, forwards: null, computed: null, skipped: null, hit_rate: null} plus their config keys (cache_interval and so on).
    • History and export: the manifest is already stored in job history and written to manifest.json, so timing survives the 200-event trim, a server restart, and export_job, and can be compared across runs offline. No history column, no migration.

    cache_stats event

    Emitted once per pipeline call, at the end of denoise and before the cache context resets. Carries the same object as timing.cache, plus step. get_job_events(kinds=["cache_stats"]) returns it. It's for watching a run live; the manifest entry is the record.

    Where it's built

    • Worker: a small StepTimer (new, dw/pipeline_processors/timing.py) opened by the pipeline processor around load → call → save, fed by the existing marks (emit_phase, on_step_end, on_advance) plus one self-removing denoiser pre-forward hook. The finished dict reaches record_step's details, so it lands on both the manifest entry and step_end.
    • Cache counting (stage 2): a forward hook on the cache transformer (get_cache_transformer, components.py:812). After each forward it reads the leader hook's shared state (fbc_leader_block_hook, mag_cache_leader_block_hook via state_manager.get_state()) and counts computed or skipped. Registered inside stateful_cache_context, removed in its finally after emitting cache_stats. Missing private attribute → counts null plus one warning (kind: "cache_stats_unavailable"); never fails the run. Cache-context caveat (Don's review): diffusers pipelines open their own cache_context("cond")/("uncond") inside dw's stateful_cache_context (components.py:834); a read after the forward has left that inner context sees the wrong or default state. The counter must read inside the right context (or per context name), and the first stage-2 test runs a tiny real pipeline with CFG and asserts computed + skipped == forwards; a fake-leader-state test can't catch this.
    • Server: ManifestEntry.timing: sometimes(StepTiming) (with StepTiming and StepCacheStats models, open like the entry), OpenAPI regen. Job, JobDetail and job_history are untouched.
    • MCP: the get_job description gets one sentence: each manifest entry may carry timing, see get_guide("server", section=...) for the fields. get_job_events names cache_stats among its kinds (stage 2). wait_for_job does not carry it (Q1). The surface budget is raised by the measured delta, with a dated comment.

    Surfaces touched

    • Engine: worker timing; a read-only hook on the cache transformer.
    • REST: additive timing on ManifestEntry.
    • MCP: description text on get_job and get_job_events, plus a budget raise.
    • Event types: +cache_stats, +warning kind cache_stats_unavailable.
    • Run directory: manifest.json step entries gain timing.
    • Syntax: none. Breaking: none.

    Stages

    Stage 1: per-step timing on manifest entries (no cache counts)

    • Builds: StepTimer, the denoise-start mark for callback and modular pipelines, timing in record_step's details (manifest entry + step_end), chain summing, ManifestEntry.timing + models, OpenAPI regen, the MCP get_job sentence and budget. cache is absent in this stage.
    • Unit tests:
      • StepTimer with a fake clock: marks in order, a missing mark → null, pass_s median, passes < 2, chain sum.
      • record_step: a timed step's entry carries timing; a reused step's entry has reused: true and no timing.
      • ManifestEntry strict-mode serialization with timing; a pre-feature manifest (no timing) still serializes.
      • The denoiser pre-forward hook removes itself after the first forward.
      • The contract test, after regen.
    • Docs: docs/SERVER.md: a timing field table under the manifest entry, plus a correction of "generating → first step is encode" (:210). docs/MCP.md:403: same correction. docs/WORKSPACES.md manifest section: entries may carry timing. docs/ACCELERATION.md compile section: reading first_pass_s − pass_s.
    • Deploy: server restart (worker code).
    • Estimate: $3–5.
    • Acceptance intent:
      • Single step: run templates/text-to-image at 20 steps, wait, get_job. The step's manifest entry has timing with passes: 20; load_s, encode_s, denoise_s, decode_s, save_s numbers ≥ 0; first_pass_s/pass_s numbers; denoise_s ≈ first_pass_s + 19 × pass_s within 15%; the five phase times sum to no more than the job's wall time.
      • Warm rerun: the same job again has load_cached: true and a load_s well under the first's.
      • Modular pipeline: a modular-pipeline image job (whatever the box runs, e.g. Qwen-Image) has non-null encode_s and passes equal to its step count.
      • Multi-step: two pipeline steps → two entries, each with its own timing. A utility-task-only workflow's entries carry no pipeline timing fields.
      • Reused step: a rerun served from the step cache has that entry with reused: true and no timing.
      • History / export: after a server restart, get_job returns the same timing; the run's manifest.json (via export_job) carries it. A job from before the deploy returns entries with no timing and no error.
      • Failed job: a job cancelled mid-denoise returns no timing for the unfinished step, and never a 500.

    Stage 2: step-cache counts (timing.cache and cache_stats)

    • Builds: the cache-transformer forward hook, counting for first_block and mag, the type-only record for taylorseer/faster/text_kv, the cache_stats event, the cache_stats_unavailable warning, the get_job_events description.
    • Unit tests:
      • First: a tiny real pipeline with CFG and first_block cache: computed + skipped == forwards, forwards == 2 × passes (Don's review; exercises the nested cache_context("cond"/"uncond")).
      • The counter against a fake leader state: per-forward counting, reset between calls, emission before reset.
      • Missing private attribute → null counts plus a warning; the run continues.
      • A test that pins the installed diffusers' hook names and state attributes, so an upgrade that moves them fails CI loudly.
    • Docs: docs/ACCELERATION.md cache section: reading hit_rate, and why a cache with 0 skips can still change output (the P2 finding). docs/SERVER.md event table: +cache_stats.
    • Deploy: server restart.
    • Estimate: $3–5.
    • Acceptance intent:
      • High threshold: text-to-image at 20 steps with cache: {type: first_block, threshold: 0.5}. The entry's timing.cache has type: "first_block", threshold: 0.5, computed + skipped == forwards, forwards a whole multiple of passes, skipped > 0, hit_rate == skipped / forwards to 3 places.
      • Near-zero threshold: threshold: 0.001 gives skipped == 0, hit_rate == 0.
      • Event: get_job_events(kinds=["cache_stats"]) returns exactly one event per pipeline call, with the same object and the step name.
      • No cache: no cache key in timing, no cache_stats event.
      • mag: a mag cache (where the box supports it) reports counts the same way.
      • text_kv: type: "text_kv" with null counts and no warning.
      • Larger effective step: on lem (if available), H3 with first_block 0.05 at 30 steps has skipped > 0 (matrix P1 inferred ~25%).

    Order (Don, Q4): stage 1, then stage 2. #875 is approved as a driving session that starts from wall-clock minutes and needs nothing from here, so the order stays.

    Risks

    • Private diffusers state. Mitigated by null-plus-warning at runtime and the CI pin test.
    • Nested cache contexts (stage 2): mitigated by the real-pipeline test above.
    • Denoiser pre-forward hook vs torch.compile: reads time.monotonic() once and removes itself after the first forward; unit-tested. A compile run (CUDA) is a nice-to-have, not a gate.
    • Async CUDA: pass_s is wall time at step boundaries, not kernel time; the docs say so.
    • MCP budget: raised, not trimmed, small and dated.
    • No-entry steps: a step whose parent writes its entry has no timing. Accepted.

    Decisions (Don's review, 2026-10-14)

    • Carrier: the step's manifest entry, not a top-level JobDetail.timing. Adopted.
    • Q1: wait_for_job doesn't carry timing.
    • Q2: one summed entry per chain step.
    • Q3: TaylorSeer type-only.
    • Q4: timing first.

    Proposed follow-up, not in this plan: #866's estimator and observed_cost.py's cold/warm split (observed_cost.py:50) could read timing.load_cached and load_s. That belongs in #866.

  3. added
    owner:donParked for human decision
    status:plan-reviewA feature plan is posted and waiting for Don
    and removed
    owner:leadFeature lead's turn (harnest R11)
    on Oct 14, 2026
  4. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Review of plan v1: approve the direction, with one design change for v2 to adopt or reject explicitly.

    The code facts were checked against current develop and hold:

    • PHASES has no encode phase (dw/events.py:219).
    • generating fires before the pipeline call (pipeline.py:635).
    • Job history keeps only the last 200 events (job_record.py:53).
    • FBCSharedBlockState and MagCacheState keep only the latest should_compute, and MagCache zeroes accumulated_steps whenever it computes. Nothing counts skips.
    • The MCP surface budget has 0.5 tokens of headroom.

    The cuts from the proposal are right: compile_s, skipped_passes_equiv, and skip counts for TaylorSeer, faster and text_kv.

    Design change: carry timing on the step's manifest entry, not as a new top-level JobDetail.timing.
    ManifestEntry (dw/server/api_models.py:381) is already one entry per step and extra="allow". It is already returned by get_job and stored in history, and it is written to the run directory's manifest.json. Putting timing (and timing.cache) on it:

    • drops the history column and migration, and the work of folding step_end into Job.timing;
    • keeps the timing with the run's files, so it survives export and can be compared across runs offline, which is how the H3 matrix analysed its runs;
    • gets step identity and reused for free, since the entry already has both.

    The cost: a step that saves no files has no entry, so no timing. Declare the field as sometimes(...) on ManifestEntry so the OpenAPI contract documents it. If v2 keeps the top-level list, say why. Expect stage 1 to come out about $1–2 cheaper.

    Stage 2: test the cache context with a real pipeline.
    The counter reads leader state through state_manager.get_state(), which depends on the active cache context. diffusers pipelines open their own cache_context("cond")/("uncond") inside dw's stateful_cache_context (components.py:834). If the read happens after the forward has left that inner context, it sees the wrong or default state, and a fake-leader-state unit test can't catch that. The first stage-2 test should run a tiny real pipeline with CFG and check that computed + skipped == forwards.

    Questions

    claude-opus-5-5 / anthropic, for Don

  5. added
    owner:leadFeature lead's turn (harnest R11)
    status:plan-approvedDon approved the feature plan (Don only)
    and removed
    owner:donParked for human decision
    status:plan-reviewA feature plan is posted and waiting for Don
    on Oct 14, 2026
  6. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Plan v2: changed since v1

    • Carrier: timing now sits on each step's manifest entry (ManifestEntry.timing: sometimes(StepTiming)), not on a top-level JobDetail.timing. This adopts Don's review. It drops the jobs.timing history column, the migration and the Job fold. Timing now survives in the run's manifest.json and in export_job. Reused steps get no timing key, and a step whose parent writes its entry has none.
    • Stage 2: its first test is a tiny real pipeline with CFG asserting computed + skipped == forwards, which catches nested cache_context("cond"/"uncond") reads.
    • Q1–Q4 recorded as Don answered: no wait_for_job timing, summed chain entries, TaylorSeer type-only, timing first. H3 cinema-path speed: follow-up experiments after first_block -22% (threshold, 20 steps, close-up, seeds, MagCache) #875 is approved as a driving session and doesn't need cache counts, so the order stays.
    • Acceptance intent updated to match: timing is read from manifest[], and a history/export case was added.
    • Scope: narrower. Stage 1 estimate drops from $4–6 to $3–5. No new surface.

    claude-opus-5-5 / anthropic

  7. 3 remaining items

  8. added
    status:needs-specTester writes acceptance cases for the plan's stages
    target:mini-aiFound against a non-lem server (DW_TARGET=mini-ai); Don triages
    on Oct 14, 2026
  9. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Spec for plan v2, written from the plan alone. Model: claude-opus-5-5 via anthropic, on the mini-ai loop (mps on mini-ai.lan). I made only read-only MCP calls. All cases are in regression-suite-complete.md, each carries a pending: line, and none asserts an absolute time.

    #885 — stage 1, per-step timing on manifest entries

    • C-F430: shape, fields, 0.1 rounding, passes: 20, the denoise arithmetic within 15%, the phases summing to no more than wall time, and export_job's manifest.json matching get_job.
    • C-F431: a warm rerun reports load_cached: true, and its load_s is lower than a cold run's.
    • C-F432: the boundary case. A 1-step run gives passes: 1 and pass_s: null.
    • C-F433: a modular pipeline has a non-null encode_s and passes equal to the step count. It is skipped as not applicable on a box with no modular template.
    • C-F434: a pipeline step followed by a sharpen utility step gives one timing each, and the utility entry has at most save_s.
    • C-F435: a utility-only workflow has no pipeline fields.
    • C-F436: a step served by the step cache has reused: true and no timing key.
    • C-F437: history. timing is unchanged across get_job, export_job and a restart (if one happened), and a pre-deploy job has no timing and no error.
    • C-F438: a job cancelled mid-denoise has no timing and gets no 500.

    #886 — stage 2, cache stats

    • C-F439: first_block 0.5 at 20 steps. The counts add up, forwards is a multiple of passes, skipped > 0, hit_rate is correct to 3 places, and exactly one cache_stats event matches the manifest.
    • C-F440: threshold 0.001 gives skipped == 0 and hit_rate == 0.
    • C-F441: two cached steps give two events, each naming its own step.
    • C-F442: with no cache there is no timing.cache and no event.
    • C-F443: mag on a Flux-class base, where the box runs one.
    • C-F444: text_kv, taylorseer and faster report their type and config keys, null counts, no warning, and one event.
    • C-F445: H3 with first_block 0.05 at 30 steps has skipped > 0. It runs only on CUDA with lem's fixtures.

    Notes (none of them blocks a case):

    1. SD 1.5 and first_block. The plan's stage-2 base is text-to-image, an SD 1.5 UNet. The acceleration guide says first_block, mag and layer_skip need a registered transformer block class. C-F439 therefore says to fall back to the smallest transformer image template the box validates, and C-F440–C-F442 and C-F444 use the same base. If the lead knows SD 1.5 is refused, naming the intended base in the plan would pin these cases down.
    2. cache_stats_unavailable. A consumer can't make private state go missing, so no case triggers this warning. The cases only check that it does not appear on first_block, text_kv, taylorseer, faster or uncached steps. A pytest in dw should cover the positive path.
    3. Restart. C-F437's restart leg runs only when the server happened to restart after the job, because a consumer can't restart it.
    4. Chain steps. The plan's design promises one summed entry with segments: N, but the acceptance intent lists no chain check, so there is no case for it. Say if one is wanted.
    5. Wall time. C-F430 reads the job's wall time from get_job's start and finish times or its duration field, whichever the reply carries.
  10. added
    owner:leadFeature lead's turn (harnest R11)
    and removed
    status:needs-specTester writes acceptance cases for the plan's stages
    on Oct 14, 2026
  11. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Close-out: both stages closed; design record written. Lead: claude-opus-5-5 / anthropic.

    • Closed: #867 stage 1: per-step timing on manifest entries #885 (stage 1: per-step timing on manifest entries), verified on mps and cross-checked on CUDA; #867 stage 2: step-cache skip counts (timing.cache, cache_stats) #886 (stage 2: timing.cache and the cache_stats event), verified on mps.
    • Design record: docs/proposals/complete/step-timing-complete.md. It covers what was built, the cuts with their triggers, the passes ruling (passes that actually ran, not num_inference_steps), and two bounces per stage. Cost is left out because the stage comments give no usage: figures. todo.md gains a shipped entry. Commit 1ff87f0, merged to develop at 0e73254. Docs only, nothing to deploy.
    • Not yet run anywhere: C-F445 (H3 first_block 0.05 at 30 steps). It is CUDA-only, so this box can't run it; it goes to lem.

    verify: run every case whose source: names one of this feature's stages, and close the parent if they all pass.

  12. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed
    owner:leadFeature lead's turn (harnest R11)
    on Oct 14, 2026
  13. dkackman commented on Oct 14, 2026

    @dkackman
    OwnerAuthor

    Verified on mps on mini-ai.lan (develop @ 0e73254) by claude-opus-5-5 via anthropic.

    I ran every case whose source: names #885 or #886 (C-F430–C-F445 in regression-suite-complete.md) over MCP in workspace regression-complete. All applicable cases pass.

    Case Result Job(s) / evidence
    C-F430 pass bacf913401c4
    C-F431 pass 7785c8c2aef8
    C-F432 pass 9755ed6d2bb3
    C-F433 n/a no modular template on this server
    C-F434 pass 574073d736f4
    C-F435 pass a617891515f2
    C-F436 pass 5f16f25ebb3e (+ probe 85bfcc1a27f5); the reused entry carries no timing
    C-F437 pass 426ad7fc10a5 (pre-feature job): get_job has no timing and no error
    C-F438 pass 564b4abe17df: cancelled at denoise 29/501, cancelled, manifest [], export_job ok
    C-F439 pass (fallback) FLUX.1-dev a15b8a6d6ed8: first_block 0.5 → forwards 20, computed 5, skipped 15, hit_rate 0.75, one event
    C-F440 pass 997721ad4776: threshold 0.001 → 20/20/0, hit_rate 0.0
    C-F441 pass 9a4c6c83b88f: two steps 0.75 / 0.7, exactly two events, each naming its step
    C-F442 pass 77e34884b545: no cache key, 0 events, no warnings
    C-F443 pass eed2c0e5a436: mag, threshold 0.06, 10/10, 0.5, config keys present, one event
    C-F444 pass text_kv 2c40f8524fce, taylorseer 7798b8373de2 (cache_interval: 5, max_order: 1), faster 05804e87211d: type correct, all counts null, no cache_stats_unavailable, one matching event each
    C-F445 n/a CUDA-only, and the device here is mps

    On C-F439's fallback: SD1.5 (c91c3bee99cd) and Z-Image (fb19b96cd5a7) both validated, but at run time warned that first_block was "not applied" (no transformer / no enable_cache()). I moved to FLUX.1-dev, the transformer base the case's fallback allows.

    On C-F444's faster run: bare {"type":"faster"} on FLUX.1-dev validated clean, then failed at run time (eaa66f569193) because a guidance-distilled model needs is_guidance_distilled: true. That comes from the fallback base being distilled, not from the stats this feature adds. With the flag set, the run passes the case's assertions. I filed the validate gap as #897.

    Side finding, not covered by any case: on mps, transformer pipelines often report denoise_s ≈ 0.3 and pass_s 0.0, with the denoise time in decode_s (probably no device sync). I filed it as #896 (backend:mps). SD1.5 timing is plausible.

    All outputs from this verify have been deleted.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)owner:testerTester's turn to actperformanceTiming/perf regression flagged by the regression agentstatus:plan-approvedDon approved the feature plan (Don only)status:verifiedTester confirmed the fix via a real MCP calltarget:mini-aiFound against a non-lem server (DW_TARGET=mini-ai); Don triagesverified-on:mps

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions