Repository navigation
#867 stage 1: per-step timing on manifest entries #885
Copy link
Copy link
Closed
Labels
featureWork bigger than a fix: designed with Don, built in stages (harnest R11)Work bigger than a fix: designed with Don, built in stages (harnest R11)owner:testerTester's turn to actTester's turn to actstageA child issue: one stage of a featureA child issue: one stage of a featurestatus:verifiedTester confirmed the fix via a real MCP callTester confirmed the fix via a real MCP calltarget:lem
Description
Stage 1 of #867. Plan: plan v2, "Stage 1". The plan comment is authoritative; this is a summary.
Builds
Per-step timing on each pipeline step's manifest entry, without cache counts:
StepTimer(new,dw/pipeline_processors/timing.py): worker monotonic clock around load → call → save. It is fed byemit_phase,on_step_end,on_advance, and a self-removing denoiser pre-forward hook (denoise start). For modular pipelines, denoise start is the opening of the denoise tqdm bar (progress.py:88-130).- Fields:
load_s,load_cached,encode_s,denoise_s,passes,first_pass_s,pass_s(median of passes 2..n, null if < 2),compiled,decode_s,save_s,segments. A missing mark isnull, never guessed. timinggoes intorecord_step's details (workflow_run.py:886), so it lands on the manifest entry and onstep_end. A reused step gets notiming.- Chain: one summed
timingper chain step, withsegments: N. ManifestEntry.timing: sometimes(StepTiming)(api_models.py:381), and the OpenAPI contract regenerated (scripts/dump_openapi.py,npm run gen:api).JobDetail,Jobandjob_historyare untouched.- MCP
get_jobdescription: one sentence pointing at the server guide. The surface budget (tests/test_mcp_server.py) is raised by the measured delta, with a dated comment.
Owes
- Unit tests as listed in the plan:
StepTimerwith a fake clock,record_steptimed and reused,ManifestEntrystrict serialization with and withouttiming, the pre-forward hook's self-removal, and the contract test. - Docs:
docs/SERVER.md: thetimingfield table, and a fix for the "generating → first step is encode" sentence (:210).docs/MCP.md:403: the same fix.docs/WORKSPACES.md: the manifest section.docs/ACCELERATION.md: compile, readingfirst_pass_s − pass_s.
Deploy
Server restart (worker code).
Passes when
The plan's stage 1 acceptance intent holds: single step, warm rerun, modular pipeline, multi-step, reused step, history/export, failed job. It also has to pass every pending: #<this issue> case the tester writes.
Filed by the lead, claude-opus-5-5 / anthropic.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
featureWork bigger than a fix: designed with Don, built in stages (harnest R11)Work bigger than a fix: designed with Don, built in stages (harnest R11)owner:testerTester's turn to actTester's turn to actstageA child issue: one stage of a featureA child issue: one stage of a featurestatus:verifiedTester confirmed the fix via a real MCP callTester confirmed the fix via a real MCP calltarget:lem
Activity
feat(engine): #885 - step timing tests and docs
dkackman commentedon Oct 14, 2026
Stage 1 shipped:
develop@ 4446338 (merge offeat/867-1-step-timing: 6342ab3 engine, 7dd0bf8 tests + docs). The driver deploys it to mini-ai before verification. I did not deploy.Plan items covered
StepTimerindw/pipeline_processors/timing.pyuses the worker's monotonic clock and travels ambiently in a contextvar.run_step(dw/workflow_run.py) opens one per pipeline step.create_step_action.load_cachedcomes from the cached and fresh-load paths inpipeline_ownership.py.Pipeline._call_pipeline.on_step_end/on_advance, and save start/end aroundsave_step.load_s, load_cached, encode_s, denoise_s, passes, first_pass_s, pass_s(median of passes 2..n, null under 2),compiled, decode_s, save_s, segments. A missing mark gives null, and every_sis rounded to 0.1.timingis inrecord_step's details, so it reaches the manifest entry (API, MCPget_job, runmanifest.json) andstep_end. A reused step gets none. A chain is summed into one timing withsegments: N. There is nocachefield.ManifestEntry.timing: sometimes(StepTiming), withStepTimingopen like the entry. OpenAPI and the UI types are regenerated, and the contract test passes. JobDetail, Job and job history are untouched. Not a breaking change: the field is additive.get_jobdescription gains one sentence. The surface budget goes from 14_700 to 14_728 by the measured delta, with a dated comment.tests/test_step_timing.py. They include one pipeline step run through the real step loop, with onlyPipeline.loadstubbed.pipeline_stepis encode" claim is corrected to say it is encode plus the first pass.Deviations from the plan
workspaces, notserver. The plan hasget_jobpoint atget_guide("server", section=...), but SERVER.md is not a served guide:get_guide("server")answers "No guide named 'server'". So the field table is### Step timingin WORKSPACES.md, under Runs.get_jobnamesget_guide("workspaces", section="Step timing"), and SERVER.md, MCP.md and ACCELERATION.md link to it.timingat all. This covers task and composed steps. The plan allowed "at most save_s" for them.segmentscounts every pipeline call. A step that calls its pipeline more than once (chain segments, orStep.runiterations) gets those calls summed.first_pass_sis then the sum of each call's first pass.denoise_s= the whole call, withencode_s,decode_s,first_pass_sandpass_snull andpassesnull. A pipeline step that never reaches_call_pipelinehassegments: 0and null call fields.compiledisfalse:apply_compileis skipped there. Timings are MPS wall times, several times CUDA's. Checkingfirst_pass_s − pass_sas a compile cost needs CUDA (lem).Pre-existing failures, not from this stage: 17 kandinsky6 tests fail on this checkout identically with and without the change (
test_examples,test_component_type_errors,test_plugin_skillsTestKandinsky6Skill). There are no new failures, and ruff is clean.— claude-opus-5-5 (anthropic), lead
dkackman commentedon Oct 14, 2026
Architecture review queued by the loop driver: this hand-off changed
dw/ordw_mcp/ondevelop(7e1bfdf425..4446338114: dw/pipeline_ownership.py dw/pipeline_processors/pipeline.py dw/pipeline_processors/progress.py dw/pipeline_processors/timing.py dw/server/api_models.py dw/workflow_run.py dw_mcp/tools_jobs.py). The reviewer checks it againstdocs/ARCHITECTURE.mdbefore the tester verifies.dkackman commentedon Oct 14, 2026
Architecture review: bounced (reviewer, claude-opus-5-5 / anthropic)
What I read:
git diff 7e1bfdf4..44463381underdw/anddw_mcp/(6342ab3, 7dd0bf8), checked againstdocs/ARCHITECTURE.mdand item 3 ofstage-c-guardrails.md.Findings
The map: new owner with no row.
dw/pipeline_processors/timing.pyis a new module and owns a new concept: per-step timing (StepTimer,timed_step,mark,denoise_start_hook,is_compiled). Its rule crossesworkflow_run.py,pipeline.py,progress.pyandpipeline_ownership.py, andManifestEntry.timingexposes it. The diff does not touchdocs/ARCHITECTURE.md.dw/pipeline_processors/timing.py:StepTimer,timed_step,mark. Rule, for example: only a pipeline step that ran is timed, on the worker's monotonic clock; a missing mark is null; a reused step has notiming. Enforced by: tests intests/test_step_timing.py, asfile::Class::test.Module size:
dw/workflow_run.pygoes over the ceiling. It was 1,093 lines and is 1,119 after this change, counted the wayscripts/arch_metrics.pycounts them (splitlines()). That is overSIZE_CEILING= 1,100.docs/stabilization/baseline.jsonpinsmodules_over_size_ceiling: 0, soarch_metrics.py --checkfails on this tree. Evidence: the map row Module and function size.workflow_run.pyback to 1,100 lines or fewer. One way is to move therun_step/_run_started_stepsplit, or the timing wiring, out of the module. Don't raise the ratchet (that needs Don'sarch-approved).Checked, no finding
_denoiserusescomponents.get_cache_transformerinstead of re-deriving it. The modular denoise-start mark sits insideprogress.py's existing bar patch. Load-cached marks sit at the existing cache and fresh paths inpipeline_ownership.py._runs_a_pipelinerepeats the"pipeline"/"pipeline_reference" in steptest already inlined invalidation.py:523andobserved_cost.py:141. No row owns that test, so it's a note, not a finding.statistics.medianandtime.monotonic, and torch's own forward pre-hook. Nothing inpyproject.tomlalready does this.timing.py's module docstring. NoCLAUDE.mdchanged.dw_mcp/tools_jobs.py: one docstring sentence, and nodwimport.18 remaining items
dkackman commentedon Oct 14, 2026
Bounce fixed (verify, C-F430
passesN+1):develop@ c07730c (merge of 23bb31d). The driver deploys it to mini-ai before verification. I did not deploy.Ruling:
passesreports the model passes that actually ran. It does not report the requested step count. Your hypothesis is right, and I checked it against the installed diffusers.PNDMScheduler(skip_prk_steps=True).set_timesteps(n)gives these timestep counts:num_inference_stepsThat matches every
passesvalue you saw, including the 1 → 1 exception. SD 1.5's PLMS mode repeats the second-to-last timestep, so the UNet really does run 21 times at 20 steps.load + passes × pass_s. Reporting 20 would makepass_sanddenoise_sdisagree withpasses.What changed
docs/WORKSPACES.mdStep timing:passesis defined as one pass per iteration of the denoise loop (one per scheduler timestep), which can differ fromnum_inference_steps. The PNDM N+1 case is named, and the row givesdenoise_s ≈ first_pass_s + (passes − 1) × pass_s.StepTiming.passesgets the same description indw/server/api_models.py. OpenAPI and the UI types are regenerated. Only a description changed, so this is not a breaking change.Plan deviation: the plan's single-step acceptance literal,
passes: 20withfirst_pass_s + 19 × pass_s, assumedpasses == num_inference_steps. That doesn't hold for SD 1.5's PNDM. The intent ispasses= the scheduler's timestep count (21 here), withfirst_pass_s + (passes − 1) × pass_s. Please file the C-F430 amendment you offered, along with the C-F434/C-F435 case fixes.C-F433 (modular): there's no modular image template on mini-ai, so this one still needs lem or another box. The code path is the denoise bar opening in
progress.py.pytest: 14182 passed. The only failures are the same 17 kandinsky6 ones as before, and ruff is clean. Plan coverage and the earlier deviations are as in the first hand-off comment.
— claude-opus-5-5 (anthropic), lead
dkackman commentedon Oct 14, 2026
Architecture review queued by the loop driver: this hand-off changed
dw/ordw_mcp/ondevelop(cd0bdba204..c07730c756: dw/server/api_models.py). The reviewer checks it againstdocs/ARCHITECTURE.mdbefore the tester verifies.dkackman commentedon Oct 14, 2026
Architecture review: passed (reviewer, claude-opus-5-5 / anthropic)
What I read:
git diff cd0bdba2..c07730c7underdw/anddw_mcp/. That is #885's commit 23bb31d, which changes onlydw/server/api_models.py. I also read the commit'sdocs/WORKSPACES.mdchange and the regeneratedui/src/lib/generated/files. I checked them againstdocs/ARCHITECTURE.mdand item 3 ofstage-c-guardrails.md.The change only rewrites the text of
StepTiming.passes'sField(description=...). No code paths or field shapes changed.ARCHITECTURE.md:34) defers the shape toManifestEntry.timinganddocs/WORKSPACES.mdStep timing, and both were updated in the same commit. The UI's response contract row (:197) wantsui/src/lib/generated/to matchapi_models.py.openapi.jsonandapi-schema.tscarry the same new description string.CLAUDE.mdchanged.dw_mcp/: untouched in this range.Not run here: I have no shell for
scripts/arch_metrics.py --checkortests/test_api_contract.py. My check that the generated files match is a read of the diff, not a test run. Thepassesruling (scheduler timesteps, notnum_inference_steps) is about behaviour, so it is the tester's to verify.dkackman commentedon Oct 14, 2026
Re-verify on mini-ai: the
passesruling holds. Handed to lem for C-F433. Server: mini-ai (mps on mini-ai.lan), develop @ c07730c. Tester: claude-opus-5-5 via anthropic.Bounce point (C-F430
passes): confirmed against the rulingWorkspace
regression-complete.<T2I-20>is the SD 1.5 template withnum_inference_steps: 20and seed 42.run_workflow(<T2I-20>, note="C-F430 timing shape (#885 re-verify)"), job a72c56f848ee.main.timingcame back as:first_pass_s + (passes−1)×pass_s= 0.4 + 6.0 = 6.4, againstdenoise_s7.0. That is 9% off, within 15%.Adjacent check, 5 steps, job 6aaee249dd84:
passes: 6, which is PNDM's timestep count. 0.4 + 5×0.3 = 1.9, againstdenoise_s2.1, so 10% off. This run was warm:load_cached: true,load_s0.0.Both jobs are deleted.
I passed C-F431, C-F432, C-F435 (with its
idfix), C-F436, C-F437 and C-F438 on 415bb5e last round. The only change since then is thepassesdescription text, so I did not re-run them.Suite cases that are wrong against the plan (not counted)
passes: 20and the 19× arithmetic. The lead's ruling makes those literals wrong against the plan. I've added the ruling to the open amendment, suite (mini-ai): C-F434/C-F435 wrong against #867 plan v2; C-F430 passes literal pending #885 ruling harnest#110, along with the C-F434 and C-F435 fixes already filed there.passesas the scheduler timestep count (21 for SD 1.5 at 20 steps).Why lem: C-F433 (modular) can't run here
owner:tester+status:fixed-pending-verifyand switchingtarget:mini-ai→target:lem.For lem's tester:
pending: #885lines and close the stage.pendinglines are still in the suite, because C-F433 hasn't run.dkackman commentedon Oct 14, 2026
Correction to the comment above (claude-opus-5-5 via anthropic): the ruling went to a new suite request, dkackman/harnest#111 (a follow-up to #110), because the guard doesn't allow comments on harnest from mini-ai.
dkackman commentedon Oct 14, 2026
Verified on lem (CUDA). Closing the stage. Server: lem (cuda:0 RTX 3090), develop @ fb829e7. Tester: claude-opus-5-5 via anthropic.
C-F430 CUDA cross-check, against the lead's
passesrulingrun_workflow(<T2I-20>, seed 42, note="C-F430 timing shape (#885 CUDA cross-check)"), job 81820e84c3b0, which succeeded with a wall time of 17.3 s.main.timingcame back as:passesis 21, which is PNDM's timestep count, as on mps.denoise_s9.4, so 4% off.C-F433 (modular)
list_workflows(shape="image")lists no modular image template. The only modular templates in the catalog are MiniMax Music3 and MiniMax H3.templates/minimax/music: oneModularPipelinestep,audio_duration30,lyrics"[inst]", seed 42,num_inference_steps: 8, saved directly.validate_workflowreturned valid. Job 0e0495b626f0 succeeded with a wall time of 66.3 s.generate_music.timing, identical onget_job,step_endandworkflow_end:encode_sis non-null. That was the modular finding the case was written for.Every mark lines up with
get_job_events:load_s9.5semantic_generator)denoiselog 45.4 = 35.2encode_s35.1denoise_s16.2pipeline_step56.6 = 11.2first_pass_s11.3decode_s3.5save_s0.7So the denoise start is the bar opening, as the plan says. Phases sum to 65.0 s, no more than the 66.3 s wall time. 11.3 + 39×0.1 = 15.2 against 16.2, so 6% off.
passesis 40, not 8, and 40 is what ran. Everypipeline_stepevent saystotal_steps: 40. The pipeline ignored thenum_inference_steps: 8I passed, so the case'spasses: 8literal couldn't be exercised this way. Under the lead's ruling (passes= model passes that actually ran), 40 is right.Side note, not a #867 stage 1: per-step timing on manifest entries #885 defect.
validate_workflowacceptednum_inference_stepson Music3 without a warning, and the run silently didn't use it. I haven't confirmed which argument Music3 actually takes, becauseget_pipeline_signature("MiniMaxAI/MiniMax-Music3")returned Not Found. Recorded here instead of filing a guess.The stage as a whole
passes: 20literalspending: #885from C-F430–C-F438. C-F430/C-F434'spassesliteral and C-F433's "image template / 8 steps" wording stay as written until denoise_total_steps reports num_inference_steps - 1 on every H3 run (9 to 8, 20 to 19): a dropped step or an off-by-one in progress? #110/Develop #111 are ruled.— claude-opus-5-5 (anthropic), tester