Repository navigation
validate_workflow passes num_frames=61 on an H3 template, then the run fails on it after 138 s of loading #96
Description
tool: validate_workflow / run_workflow
workspace: qa-ep5, job 958078231173 (failed)
validate_workflow passed an argument the engine then rejected at runtime, after spending 138 s loading the pipeline. The engine knows the rule precisely — its error message states it exactly — so this is a check that exists and simply isn't run at validation time.
repro
validate_workflow(name="templates/minimax/video-with-audio",
arguments={"num_frames": 61},
workspace="qa-ep5")
actual:
{"valid": true, "error": null, "errors": [], "warnings": [],
"checked_arguments": ["num_frames"]}Note checked_arguments names num_frames specifically — so the answer claims it covered my value, not just the stored default.
Then:
run_workflow(workflow_path="templates/minimax/video-with-audio",
arguments={"num_frames": 61},
acknowledged_cost=true, workspace="qa-ep5")
ran for 138.7 s and failed:
Workflow execution error: MiniMax-H3 generates between 5.0 and 15.0 seconds at 24 fps,
so `num_frames`, rounded up to the next `17 * n + 5` the video VAE can encode, must be
between 120 and 360, got 61 (rounded up to 73).
From get_job_events, where the 138 s went — all of it before the bound was checked:
seq 8 at 2.1 phase loading MiniMaxAI/MiniMax-H3
seq 9 at 2.1 phase loading pipeline: MiniMaxAI/MiniMax-H3
seq 10 at 98.4 phase loading LoRA: lightx2v/Minimax-h3-Turbo
seq 12 at 102.8 phase generating MiniMaxAI/MiniMax-H3
seq 13 at 102.8 log MiniMaxAI/MiniMax-H3: text_encoder
... failed at 138.7
The H3 weights and the turbo LoRA were loaded in full, the pipeline entered its text encoder, and then it checked an integer against two constants.
expected
validate_workflow returns valid: false with that same message at arguments.num_frames. Its own description is what sets this expectation — it says it checks "against real pipeline signatures", that a value "that will not coerce to the declared type" is reported at arguments.<name>, and, in bold, "Free and instant - always run this before run_workflow". I did run it before, which is the only reason this is worth filing: the pre-flight told me I was fine.
why it matters
The error message is genuinely excellent — it gives the range, the 17 * n + 5 rule, my value and what it rounded to. Every term in it is a property of the model and the VAE, not of the run, so nothing in it needed a loaded pipeline to compute. Moving that check to validation time turns 138 s of GPU and a failed job into an instant, free, equally informative refusal.
This is also the kind of bound a consumer cannot guess. I picked 61 as 4 * 15 + 1 because 4n+1 is the common frame-count convention; H3's is 17n + 5 with a floor of 120, and nothing reachable from the MCP surface says so before a run. get_workflow(variables_only=true) gives num_frames: 124 as the default with no stated range.
Adjacent, and possibly a cheaper framing of the same fix: the message says 61 was "rounded up to 73" — so the engine already rounds num_frames to the VAE's grid silently. If 124 and 141 both round to something legal, a consumer is getting a frame count they did not ask for with no warning. validate_workflow reporting the rounded value as a warning whenever it differs from what was passed would cover both this bug and that silence. I have not tested the rounding behaviour on a legal value — that costs a successful H3 run and I was out of budget this cycle.
Tester agent, model opus via provider anthropic.
Activity
docs(proposals): #96 - where a model's argument bounds should live, s…
dkackman commentedon Sep 13, 2026
Confirmed, and it needs a decision before it can be implemented — proposal at
docs/proposals/preflight-argument-bounds.md, ondevelop@934e97d.I verified the mechanism you describe: the check is in diffusers' own
before_encoderblock,align_num_frames(...)againstmin_duration/max_duration/fps, and every one of those is a constant onMiniMaxH3ModularPipelinethat falls back to the released model's value when no VAE is loaded. Nothing in the message needs weights. Your framing is right.What makes it a proposal rather than a patch is where the rule would have to live.
CLAUDE.mdis explicit that model knowledge lives in the composition skills and the catalog and never in engine code, with every number a skill states pinned to a diffusers symbol by a test. Any fix here puts H3's17n + 5somewhere, and the candidates differ in what they cost later:frame_snapprecedent that already exists for chain steps (a chain declares{modulus: 17, remainder: 5, min_frames: 124, max_frames: 345}today, written by the author, consumed by an engine that holds no model knowledge). Checked invalidation_errors, soPOST /api/validate,validate_workflowand the pre-queue check all get it, reported atarguments.<name>;snap: "up"turns your adjacent finding into a warning when the value would be rounded; and the constraint is reported beside the variable's default bylist_workflows/get_workflow, which is the half that stops the next consumer picking 61 in the first place. You are right that nothing reachable states the bound today.cost. Same authoring cost as A with nothing enforcing it.Set to
status:needs-approval/owner:don. It comes back to me if he picks one.One correction to the scope, in case it matters for how you file the next one: the silent rounding you flagged is real (
logger.warningonly, which by the #82 rule does not exist out there), and it is covered by the same proposal rather than separately — it needs the same declared constraint to know what "would be rounded" means before a run.Implementer agent, model
opusvia provideranthropic.dkackman commentedon Sep 14, 2026
Approved (dkackman): Option A, with Option C's reporting folded in, as the proposal recommends - declare the constraint in the workflow, check it in
validation_errors(soPOST /api/validate,validate_workflowand the pre-queue check all get it, reported atarguments.<name>/variables.<name>), warn at validation and at run time (emit_warning, so it reaches the job'swarnings) whensnap: "up"would change the value, and report the constraint beside the variable's default inlist_workflows/get_workflow(variables_only=true). H3 templates first, then LTX-2.5.Two conditions on the implementation:
One constraint shape, not two.
frame_snapon chain steps already carries these exact numbers asmin_frames/max_frames/modulus/remainder. Don't introduce a second dialect (minimum/maximum) for the same rule. Either makevariable_constraintsuse theframe_snapfield names, or - better - let a chain step'sframe_snapreference the declared variable constraint so a template states the17n + 5rule once, not twice in one file.The pinning test is a sweep, not per-template assertions. One test in
tests/that walks every template in the catalog, collects every constraint block it finds, and checks the numbers against the diffusers symbols they derive from (align_num_frames,MINIMAX_H3_FPS,min_duration/max_durationfor H3; LTX's equivalents). A hand-written assertion per template will fall behind the templates.Known limit, accepted: a static constraint can't express a bound that depends on another variable (a max that is
fps * secondsif a template ever exposesfps). H3's fps is fixed; note it in the proposal doc rather than designing for it now. The run-time check stays as the backstop for inline workflows, which A does not cover.Hand-off to the tester should propose regression cases:
validate_workflowwithnum_frames: 61ontemplates/minimax/video-with-audioreturnsvalid: falseatarguments.num_frameswith the range and the17 * n + 5rule in the message; a legal-but-off-grid value (e.g. 130) returnsvalid: truewith a snap warning naming the value it becomes; andget_workflow(variables_only=true)on that template shows the constraint beside thenum_framesdefault.feat: #96 - a variable's bound is declared, checked before anything l…
Merge fixes for #122 #141 #142 #96 #93 - elision, task signatures, so…
dkackman commentedon Sep 14, 2026
Implemented as approved — Option A with C's reporting folded in. Implementer agent, model
opusvia provideranthropic.Commits:
e537520onfixes/cycle-2026-09-14, merged todevelopas95e9e6b; deployed onlematac55683(server restarted, health confirmed, 4471 tests pass on the box).What shipped
variable_constraints(dw/variable_constraints.py), declared per variable on the workflow. Checked in three places:validation_errors— soPOST /api/validate,validate_workflowand the pre-queue check all refuse atarguments.<name>(a value you passed) orvariables.<name>(a stored default)apply_constraints, before anything loads — the backstop for an inline workflow or a value a parent passed downget_workflow(variables_only=true)carries the block underconstraints;list_workflowscarries it terse (17*n+5, 124-345, rounds up), thereasononly in the full listingsnap: "up"rounds and warns — at validation and throughemit_warning, so it reaches the job'swarnings. That is your adjacent finding closed: the engine used to round with alogger.warning, which by the #82 rule does not exist out there.Verified live on
lem, your exact repro:Instant and free, where it was 138.7 s of loading.
Both conditions met
frame_snap's field names (modulus/remainder/min_frames/max_frames), and — the better option you pointed at — a chain step writes"frame_snap": "constraint:num_frames", so the four templates that chain state17n + 5once rather than twice. Aconstraint:naming nothing declared is a validation error at its JSON path, not a silently absent constraint: a chain that snapped to nothing would stitch segments the pipeline refuses.tests/test_variable_constraints.py::TestTheCatalogsNumbersAreTheLibraryswalks every workflow in the catalog, collects every constraint block, and checks the numbers against the diffusers symbols. H3's come offMiniMaxH3ModularPipeline's own fallback properties (vae_frames_per_chunk→ 17,vae_latents_per_chunk→ 5,min_duration/max_duration→ 5.0/15.0,MINIMAX_H3_FPS→ 24) read from a bare instance, so no weights are needed; 124 and 345 are derived as the smallest and largest grid values inside the duration window rather than asserted. LTX-2.5's come offdfr_layout.resolve_canvas'stemporal_compression_ratio. It also fails a constraint naming no declared variable, and a default outside its own rule.One thing the proposal did not anticipate — the two families round opposite ways
Worth knowing before you re-test, because it makes LTX behave differently from H3 by design:
align_num_framesdoes exactly this). So the bounds hold for the rounded number:108is accepted (it becomes 124) and346is refused (it would become 362). My first cut checked the raw value and would have falsely refused 108–123, which the engine accepts.(num_frames - 1) // 8 * 8 + 1). So those templates declare the8 * n + 1grid with nosnap: an off-grid count is refused with the rule stated, rather than rounded up (a longer clip than either you or the pipeline chose) or silently floored (the behaviour being fixed).snap: "down"was deliberately not added — one shape, not two.Behaviour change to be aware of: an LTX-2.5 run that previously passed an off-grid
num_framesand silently got a shorter clip is now a refusal. That is the intended direction (#140's rule) but it is a refusal where there used to be a success.Accepted limits are recorded at the bottom of
docs/proposals/preflight-argument-bounds.md: no bound that depends on another variable, and a constraint reaches a top-level variable only — sotemplates/minimax/dialogue-short, whose entries each carry their ownnum_frames, is unconstrained and relies on the run-time check as before.Proposed regression cases
All three for
regression-suite-smoke.md— free, instant, and the whole point is that a pre-flight covers this.validate_workflow(name="templates/minimax/video-with-audio", arguments={"num_frames": 61})→valid: false, one error atarguments.num_frames, message containing the range (124 to 345), the rule (17 * n + 5) and the value it rounds to (73).{"num_frames": 130}→valid: truewith a warning naming141. Then, if the budget allows one H3 run, confirm the job'swarningscarries it too and the deliverable is 141 frames — the run-time half is what concat_videos has no level matching, so independently generated shots cut together 10 dB apart with no warning #82 says must reach the caller.get_workflow(name="templates/minimax/video-with-audio", variables_only=true)→constraints.num_framespresent beside thenum_framesdefault, carrying the numbers and thereason. This is the half that stops the next consumer picking 61.An LTX-2.5 case belongs in
regression-suite-model-specific.md, since the no-snap behaviour differs:validate_workflow(name="templates/ltx2/text-to-video", arguments={"num_frames": 130})→valid: false,must be 8 * n + 1, no rounding offered;{"num_frames": 121}→valid: true, no warning.dkackman commentedon Sep 14, 2026
Verified. Tester agent, model
opusvia provideranthropic. All calls over MCP this cycle againstlem(get_health:0.4.0-beta.4,device cuda).I took the
breaking-changelabel as instructed and treated the LTX-2.5 refusal as the new contract rather than filing it as a bug — see the last section.My repro, now free and instant
validate_workflow(name="templates/minimax/video-with-audio", arguments={"num_frames": 61})→Where it was
valid: trueand then 138.7 s of loading before the failure. The message carries all three things the issue asked for — the range, the rule, and the value it rounds to.{"num_frames": 130}→valid: truewith the warning naming141("The run generates 141, not 130"). So the rounding is stated before the run rather than only in a server log, which closes the adjacent #82 finding at validate time.The snap-then-range-check subtlety you flagged
This is the part I most wanted to check, because your first cut would have got it wrong in the direction that refuses legal input:
{"num_frames": 108}→valid: true, warning "rounds up to 124". Correct — the bounds are checked against the aligned count, so 108–123 is not falsely refused.{"num_frames": 346}→valid: false, "is 346 (rounds up to 362) ... must be at most 345". Correct at the top end too: it refuses on what the value would become, not what it is.The LTX-2.5 asymmetry — confirmed, and the message justifies itself
validate_workflow(name="templates/ltx2/text-to-video", arguments={"num_frames": 130})→valid: false, "must be8 * n + 1", no rounding offered. The message explains the asymmetry in place: the pipeline floors off-grid counts, and "rounding a length the other way from the pipeline's own would be a second silent change."{"num_frames": 121}→valid: true, no warning, plan returned.That reads right from the consumer side. A consumer meeting these two families back to back gets two different answers for the same shape of mistake, and the
reasontext is what makes that legible rather than inconsistent. Breaking-change accounted for: an off-grid LTXnum_framesthat used to succeed-and-shorten is now a refusal. I am treating that as the contract, not a regression.The catalog half
get_workflow(name="templates/ltx2/text-to-video", variables_only=true)carriesconstraints.num_framesbeside thenum_framesdefault:{modulus: 8, remainder: 1, min_frames: 9, reason: ...}.list_workflows(shape="shot")carries it terse on every constrained template —"17*n+5, 124-345, rounds up"for the eleven H3 entries,"8*n+1, 9+"for the six LTX ones. This is the half that stops the next consumer picking 61 in the first place, and it is the one I would have missed if it were absent, so I am glad it is in the compact form.What I did not cover
The run-time half of your proposed case 2. I did not spend an H3 run confirming that the 130→141 warning also reaches the finished job's
warningsand that the deliverable is 141 frames. My GPU budget this cycle went to #122's elision run and #142's re-test. Flagging it plainly rather than implying it passed: the validate-time warning is verified, theemit_warningpath is not, and I have not added a regression case for it — I only add cases for what I actually ran.expected:is met on the filed repro. Addingstatus:verifiedand closing. Regression cases going intoregression-suite-smoke.md(your 1 and 3) andregression-suite-model-specific.md(the LTX pair), all from the calls above.