Skip to content

Spare 1-D norm weights from a blanket --type - #2

Merged
danielhanchen merged 1 commit into
masterfrom
fix/spare-1d-norm-weights-from-blanket-quant
Aug 8, 2026
Merged

Spare 1-D norm weights from a blanket --type#2
danielhanchen merged 1 commit into
masterfrom
fix/spare-1d-norm-weights-from-blanket-quant

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Aug 7, 2026

Copy link
Copy Markdown
Member

tensor_should_be_converted has no rule for 1-D weights. A per-channel norm scale survives a blanket --type only by accident: when its length does not divide the quant block size, or when one of the FLUX-era name rules happens to match it.

FLUX gets away with it because q_norm/k_norm are [128] and 128 % 256 != 0, so the existing block-size check spares them. MiniMax-H3's per-block norms are [5376], and 5376 % 256 == 0, so a blanket --type q4_K quantizes 105 of them.

Why this is worth catching

The result loads and renders a plausible video, so a "does it run" check passes it. Scored against a bf16 render of the same prompt and seed (640x384, 25 frames, 4 steps, cfg 1.0, seed 1234, --rng cpu, text encoder on CPU):

build 1-D tensor types PSNR SSIM LPIPS
q4_K blanket, before F32 5, F16 51, BF16 106, Q4_K 105 9.87 0.074 0.981
q4_K blanket, after F32 5, F16 51, BF16 211 22.22 0.841 0.292

SSIM 0.074 means the output is essentially uncorrelated with the reference. Peak VRAM is unchanged at 16.83 GiB, and the file grows 0.77 MiB on 10.60 GiB (0.007%).

The change

One branch, next to the existing block-size check: if the target type is quantized and the tensor is 1-D, leave it alone. A 1-D weight is a per-channel gain, never a matmul weight. Every channel of a block would share one scale and one min, and a gain vector has no reason to be locally smooth, so quantizing it buys almost nothing.

The 1-D rule alone is sufficient

The workaround for this today is --tensor-type-rules 'norm[0-9]*\.weight$=bf16,condition_proj\.weight$=bf16'. That build produces the same 1-D layout and scores 21.52 / 0.838 / 0.299, so nothing here depends on also sparing condition_proj.weight, which is 2-D and a separate quality choice rather than something this rule should touch.

The two builds land within a small margin of each other in opposite directions. On a 4-step schedule a small weight perturbation moves the sampling trajectory a long way, so that gap should be read as trajectory noise, not as evidence that either treatment of condition_proj is better.

Verification

Converted the same 40 GB bf16 checkpoint with a blanket --type q4_K and no --tensor-type-rules, then read the tensor types back out of the GGUF and rendered it:

  • before: 105 1-D tensors come out Q4_K
  • after: none do, and all 211 1-D tensors keep their source type
  • rendered and scored against the bf16 reference: the table above

Cross-checked two ways. The same one-line rule applied to upstream master and to this branch produce byte-identical 1-D layouts and identical scores (22.22 / 0.8407 / 0.2921), so the result does not depend on anything else in this fork.

Builds clean on Linux with -DSD_CUDA=ON and on CPU (sd-cli and sd-server both link).

Scope

Deliberately narrow. This is the general rule that should have been there from the start, and it only affects blanket --type conversions of models whose 1-D tensors happen to divide the block size, so no existing FLUX, SD or SDXL quant changes at all. --tensor-type-rules still overrides it for anyone who wants the old behaviour on a specific pattern.

@danielhanchen
danielhanchen force-pushed the fix/spare-1d-norm-weights-from-blanket-quant branch from 2f5aaf0 to a32e7a1 Compare August 7, 2026 15:06
tensor_should_be_converted has no rule for 1-D weights, so a per-channel norm
scale survives a blanket --type only by accident: when its length does not
divide the quant block size (FLUX q_norm/k_norm are [128] and 128 % 256 != 0),
or when one of the FLUX-era name rules happens to match it.

MiniMax-H3's per-block norms are [5376], and 5376 % 256 == 0, so a blanket
--type q4_K quantizes 105 of them. The result loads and renders a plausible
video, so a "does it run" check passes it, but scored against a bf16 render of
the same prompt and seed it is destroyed.

A 1-D weight is a per-channel gain, never a matmul weight. Every channel of a
block would share one scale and one min, and a gain vector has no reason to be
locally smooth, so quantizing it buys almost nothing and costs a lot: on this
model holding all 211 1-D tensors adds 0.77 MiB to a 10.60 GiB file, 0.007%.

Measured on minimax_h3_fl2va_pruned, converted from the same bf16 checkpoint
with a blanket --type q4_K and no --tensor-type-rules, rendered at 640x384,
25 frames, 4 steps, cfg 1.0, seed 1234, --rng cpu, and scored against a bf16
render of that same prompt and seed:

    1-D tensor types                          PSNR   SSIM   LPIPS
    before  F32 5, F16 51, BF16 106, Q4_K 105  9.87  0.074   0.981
    after   F32 5, F16 51, BF16 211           22.22  0.841   0.292

SSIM 0.074 means the output is essentially uncorrelated with the reference.

The 1-D rule alone is sufficient. A build made with
--tensor-type-rules 'norm[0-9]*\.weight$=bf16,condition_proj\.weight$=bf16'
produces the same 1-D layout and scores 21.52 / 0.838 / 0.299, so nothing here
depends on also sparing condition_proj.weight, which is 2-D and a separate
quality choice. Peak VRAM is unchanged at 16.83 GiB.
@danielhanchen
danielhanchen force-pushed the fix/spare-1d-norm-weights-from-blanket-quant branch from a32e7a1 to 9f6c4fd Compare August 7, 2026 15:07
@danielhanchen
danielhanchen merged commit 7a431ec into master Aug 8, 2026
14 of 20 checks passed
@danielhanchen
danielhanchen deleted the fix/spare-1d-norm-weights-from-blanket-quant branch August 8, 2026 02:23
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.

1 participant