Skip to content

Stop MiniMax-H3 aborting on the default cfg-scale and on --vae-on-cpu - #1862

Open
danielhanchen wants to merge 1 commit into
leejet:masterfrom
unslothai:up/h3-cfg-and-audio-vae-cpu
Open

Stop MiniMax-H3 aborting on the default cfg-scale and on --vae-on-cpu#1862
danielhanchen wants to merge 1 commit into
leejet:masterfrom
unslothai:up/h3-cfg-and-audio-vae-cpu

Conversation

@danielhanchen

Copy link
Copy Markdown

Two deterministic aborts on MiniMax-H3, both of which a user hits on a first run.

1. The default cfg-scale aborts

`--cfg-scale` defaults to 7.0, and H3 aborts at any value above 1.0:

ggml/src/ggml.c: GGML_ASSERT(!ggml_is_transposed(a)) failed

SIGABRT, exit 134. So a `vid_gen` command that simply omits the flag crashes instead of rendering. H3 is distilled with guidance baked in and has no negative-prompt semantics; its empty uncond prompt encodes to zero tokens, which is where the assert comes from. Measured: cfg 1.0 renders, cfg 1.5 and cfg 4.0 both abort.

Since no cfg above 1.0 has a legitimate use on this model, this clamps to 1.0 with a warning rather than failing.

2. --vae-on-cpu aborts

ggml/src/ggml-cpu/ops.cpp:6321: GGML_ASSERT(src0->type == GGML_TYPE_F16) failed

The trigger is the audio VAE, not the video VAE. `ggml_conv_1d` and `ggml_conv_1d_dw` build an F16 im2col, and the CPU backend then asserts the kernel is F16, while the audio VAE conv kernels reach it as F32. Bisected on the flags: `--vae-on-cpu` with `--audio-vae` aborts, the same command without `--audio-vae` renders. Converting the audio VAE checkpoint to fp16 does not help, because the requirement is imposed inside the graph rather than by the file.

This casts F32 conv1d kernels to F16 in-graph, and only on CPU backends, so GPU backends keep their F32 kernels and no checkpoint is degraded on disk.

Verified end to end on a CUDA build with a q4_K H3 denoiser, the Qwen3-VL encoder and both VAEs: cfg 7 now warns and renders; `--vae-on-cpu` renders, alone and combined with `--offload-to-cpu --clip-on-cpu`.

Two independent SIGABRTs, both reachable from ordinary invocations.

1. cfg-scale. H3 is distilled with guidance baked in and has no negative prompt
semantics; its empty uncond prompt encodes to zero tokens, so building the uncond
branch trips GGML_ASSERT(!ggml_is_transposed(a)) in ggml.c. sd.cpp defaults
--cfg-scale to 7.0, so a bare `sd-cli --mode vid_gen` on H3 crashes rather than
rendering. Measured: cfg 1.0 renders, cfg 1.5 and cfg 4.0 both abort, exit 134.

There is no correct cfg > 1 behaviour to implement for a CFG-free model, so warn
and clamp to 1.0 instead of asserting deep inside ggml.

2. --vae-on-cpu. ggml_conv_1d and ggml_conv_1d_dw build an F16 im2col, and the
CPU backend additionally requires the kernel itself to be F16;
ggml_compute_forward_im2col_f16 asserts it. audio_conv_weight_type maps only
BF16 to F16 and lets F32 through, so H3's F32 audio conv kernels abort with
GGML_ASSERT(src0->type == GGML_TYPE_F16) as soon as the audio VAE decodes on the
CPU. Converting the checkpoint to fp16 does not help: the type is imposed here,
not by the file.

Cast the kernel in-graph, and only when the runner is actually on a CPU backend,
so GPU precision is untouched and no weight is degraded at load. All four
conv_1d sites in this file need it, not just the module ones: the STFT
forward_basis and the transposed-conv reversed_filter are computed F32 tensors
that reach the same assert. depthwise_conv_transpose1d therefore takes the
runner context rather than a bare ggml_context.

Verified on minimax_h3_fl2va_pruned q4_K, 640x384, 25 frames, 4 steps, seed 1234:

    case                              before      after
    --cfg-scale 4.0                   exit 134    exit 0
    cfg omitted (default 7.0)         exit 134    exit 0
    --vae-on-cpu --audio-vae          exit 134    exit 0
    full low_vram flag set            exit 134    exit 0
    baseline                          exit 0      exit 0, byte-identical
    --offload-to-cpu                  exit 0      exit 0, byte-identical

The two CPU-VAE cases differ from the baseline by 114 bytes, which is the
expected consequence of their conv kernels now running F16 on the CPU.
danielhanchen added a commit to unslothai/stable-diffusion.cpp that referenced this pull request Aug 8, 2026
The prebuilt pipeline builds leejet's source at an aged release tag, not this
fork's master, so the three MiniMax-H3 fixes on master reach nobody: every
Studio user installs a binary that still aborts on the default cfg-scale, still
aborts on --vae-on-cpu, and still quantizes H3's 1-D norms into an output
uncorrelated with its own bf16 reference.

Building from master instead would throw away the reason the pipeline is shaped
this way, which is that what we publish should be traceable to a specific
upstream release. So keep the upstream tag as the base and carry the delta
explicitly:

- patches/ holds one file per fix, each with its upstream pull request in the
  header. All three are open on leejet: leejet#1861, leejet#1862, leejet#1863.
- resolve applies them to the checked-out tag, after running git apply --check
  over the whole set so a stale patch stops the run before the tree is half
  modified. That failure is the signal to delete the patch (upstream merged it)
  or refresh it (upstream moved the code).
- a non-empty set moves the published tag to <upstream tag>-u<id>, where id is
  the sha256 prefix of the concatenated patches. The tag then says whether a box
  is stock, and a changed patch set republishes rather than matching an existing
  release and skipping.
- the manifest and the release notes both record the applied list.

An empty patches/ leaves the tag and every asset name exactly as they are today.

Verified by running the resolve step against master-813-bfbef5b with gh stubbed:
all three patches apply, the tag becomes master-813-bfbef5b-u<id>, and the
stamped source tarball contains the fixes.
@stduhpf

stduhpf commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Since no cfg above 1.0 has a legitimate use on this model, this clamps to 1.0 with a warning rather than failing.

That's quite an assumption. We could get un-distilled models in the future (like we got for Flux in the past), it would be best to find a way to make CFG work without crashing rather than overriding the users settings. (Having a default guidance settings for each model based on architecture could be a nicer solution).

Also those are two unrelated changes that would probably make more sense separated in two PRs.

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.

2 participants