fix(video): count minimax steps as transformer evaluations

MiniMaxH3Scheduler counts the terminal sigma in num_inference_steps, so
Steps N ran N-1 evaluations while every other model runs N. The shim
hands the scheduler p.steps + 1, the slider starts at 1, and the PDD
pin records the evaluation count while passing the scheduler its grid
argument. Metadata written before this change counted grid points.
This commit is contained in:
CalamitousFelicitousness
2026-09-16 00:47:46 +01:00
parent 29fe895c0b
commit 726907dd5b
6 changed files with 12 additions and 10 deletions
+4 -4
View File
@@ -285,7 +285,7 @@ def reconcile(pipe, loaded, components):
def pin(p, model):
"""Hold a generation on the distilled step count and shipped schedule while heads are installed; returns the pinned step count or None."""
"""Hold a generation on the distilled evaluation count and shipped schedule while heads are installed; returns the scheduler step argument or None."""
state = getattr(model, 'sdnext_pdd', None)
if state is None:
return None
@@ -297,13 +297,13 @@ def pin(p, model):
scheduler.set_shift(scheduler.config['shift'])
shifts[name] = scheduler.config['shift']
requested = p.steps
p.steps = state.steps
p.steps = state.heads.nfe # what the user-facing step count means: transformer evaluations
if getattr(p, 'task_args', None) is not None:
p.task_args['num_inference_steps'] = state.steps
p.task_args['num_inference_steps'] = state.steps # the scheduler argument that yields that many grid intervals
if getattr(model, 'num_timesteps', None) is not None:
model.num_timesteps = state.heads.nfe # the progress total counts transformer evaluations
extra = getattr(p, 'extra_generation_params', None)
if extra is not None:
extra.update({state.spec.shift_keys[name]: shift for name, shift in shifts.items() if name in state.spec.shift_keys})
log.info(f'Network: type=PDD name="{state.name}" steps={state.steps} requested={requested} nfe={state.heads.nfe} shift={shifts}')
log.info(f'Network: type=PDD name="{state.name}" steps={state.heads.nfe} requested={requested} grid_steps={state.steps} shift={shifts}')
return state.steps
+1 -1
View File
@@ -26,7 +26,7 @@ def create_ui(prompt, _negative, styles, overrides, script_inputs, mp4_fps, mp4_
width, height = ui_sections.create_resolution_inputs('minimax', default_width=1024, default_height=576, step=32)
btn_detect_image_size = ToolButton(value=ui_symbols.detect, elem_id="minimax_resize_detect_size")
with gr.Row():
steps = gr.Slider(minimum=2, maximum=100, step=1, label="MiniMax steps", elem_id='minimax_steps', value=30)
steps = gr.Slider(minimum=1, maximum=100, step=1, label="MiniMax steps", elem_id='minimax_steps', value=30)
frames = gr.Slider(label='MiniMax frames', minimum=22, maximum=362, step=17, value=124, elem_id='minimax_frames')
with gr.Row():
video_shift = gr.Slider(minimum=0.5, maximum=20.0, step=0.1, value=12.0, label="MiniMax video shift", elem_id='minimax_video_shift')
+3 -3
View File
@@ -29,9 +29,9 @@ def apply_overrides(p, pipe, still: bool = False, audio: bool = True, preview: b
log.debug(f'Pipeline: cls={pipe.__class__.__name__} frames requested={getattr(p, "frames", None)} aligned={frames}')
p.frames = frames
p.task_args['num_frames'] = frames
p.steps = max(2, p.steps)
p.task_args['num_inference_steps'] = p.steps
pipe.num_timesteps = p.steps - 1 # sigma grid includes the terminal point; feeds the progress total
p.steps = max(1, p.steps) # transformer evaluations, as on every other model
p.task_args['num_inference_steps'] = p.steps + 1 # the scheduler counts the terminal sigma as a grid point
pipe.num_timesteps = p.steps # feeds the progress total
if p.sampler_name not in ('None', 'Default'):
log.warning(f'Pipeline: cls={pipe.__class__.__name__} sampler={p.sampler_name} unsupported: using model default')
p.sampler_name = 'Default' # the model default is the bespoke scheduler pair, which discrete samplers must not replace
+2 -1
View File
@@ -6,7 +6,7 @@ Offline unit tests for the MiniMax schedule shift in modules.video_models.video_
- ``set_sampler_shift`` writes scheduler, default_scheduler and audio_scheduler and keys the result for infotext
- a request without values resets what the previous request set
- the Default sampler restore, a deepcopy of default_scheduler, carries the shift into the sigma grid
- ``apply_overrides`` records the applied values on the processing object
- ``apply_overrides`` records the applied values on the processing object and hands the scheduler one grid point more than the step count
No running server required.
@@ -188,6 +188,7 @@ def test_apply_overrides_records_the_applied_values():
assert p.extra_generation_params == {'Video shift': 6.0, 'Audio shift': AUDIO_SHIFT}, f'recorded={p.extra_generation_params}'
assert shifts(pipe) == (6.0, 6.0, AUDIO_SHIFT), f'shifts={shifts(pipe)}'
assert p.sampler_name == 'Default'
assert p.steps == STEPS and p.task_args['num_inference_steps'] == STEPS + 1 and pipe.num_timesteps == STEPS, f'steps={p.steps} grid_steps={p.task_args["num_inference_steps"]} total={pipe.num_timesteps}'
def test_apply_overrides_without_values_uses_the_shipped_schedule():
+1 -1
View File
@@ -335,7 +335,7 @@ def test_pin_overrides_steps_and_shift():
pipe.audio_scheduler.set_shift(2.0)
p = types.SimpleNamespace(steps=30, task_args={'num_inference_steps': 30}, extra_generation_params={'Video shift': 4.0, 'Audio shift': 2.0})
assert network_pdd.pin(p, pipe) == 9
assert p.steps == 9 and p.task_args['num_inference_steps'] == 9
assert p.steps == 8 and p.task_args['num_inference_steps'] == 9, f'steps={p.steps} grid_steps={p.task_args["num_inference_steps"]}'
assert pipe.num_timesteps == 8
assert pipe.scheduler.shift == 12.0 and pipe.audio_scheduler.shift == 3.0
assert p.extra_generation_params == {'Video shift': 12.0, 'Audio shift': 3.0}, f'recorded={p.extra_generation_params}'
+1
View File
@@ -949,6 +949,7 @@
{"id":"minimax_video_shift","label":"MiniMax video shift","localized":"","hint":"Exponential shift of the video sigma schedule, <code>sigma' = s*sigma / (1 + (s-1)*sigma)</code>. Values above 1 move the step grid toward full noise, values below 1 toward the clean end. The value is absolute and does not scale with the step count.<br><br>Default is <b>12</b>, the value the model ships with. Distilled LoRAs run at the shift they were trained with: <b>12</b> for the 544p <i>lightx2v</i> files, <b>6</b> for their 768p files. Parallel decoding (PDD) LoRAs pin the shipped value.<br><br>Recorded in the output metadata as <b>Video shift</b>.","ui":"video"},
{"id":"minimax_audio_shift","label":"MiniMax audio shift","localized":"","hint":"Exponential shift of the audio sigma schedule. The audio rows are denoised on this schedule inside the joint pass, so the value applies with audio output disabled too.<br><br>Default is <b>3</b>, the value the model ships with; the published turbo LoRAs keep it. Parallel decoding (PDD) LoRAs pin the shipped value.<br><br>Recorded in the output metadata as <b>Audio shift</b>.","ui":"video"},
{"id":"","label":"MiniMax Frames","localized":"","hint":"MiniMax is optimized to generate 5-15sec videos at 24 FPS","ui":"video"},
{"id":"minimax_steps","label":"MiniMax steps","localized":"","hint":"Number of transformer evaluations, counted the same way as on every other model. Distilled LoRAs run at the count in their name: a 4-step file at <b>4</b>, an 8-step file at <b>8</b>. Parallel decoding (PDD) LoRAs pin their own count.<br><br>Default is <b>30</b>.","ui":"video"},
{"id":"","label":"Model Info","localized":"","hint":"","ui":"component-8779"},
{"id":"","label":"Model pipeline","localized":"","hint":"If autodetect does not detect model automatically, select model type before loading a model","ui":"settings_sd"},
{"id":"","label":"Model auto-load on start","localized":"","hint":"","ui":"settings_sd"},