Skip to content

Distilled 5-6 step sampling via folded Turbo LoRA, plus three fixes - #14

Open
guyz wants to merge 5 commits into
antirez:mainfrom
guyz:turbo-fold-and-fixes
Open

Distilled 5-6 step sampling via folded Turbo LoRA, plus three fixes#14
guyz wants to merge 5 commits into
antirez:mainfrom
guyz:turbo-fold-and-fixes

Conversation

@guyz

@guyz guyz commented Aug 11, 2026

Copy link
Copy Markdown

This PR adds LoRA support to h3 by folding adapters into the bf16 checkpoint
offline, without adding a LoRA runtime. With the community Turbo distillation
adapter folded, the existing 5-6 step mode becomes usable at final quality
instead of serving as a preview mode, and runs ~1.35x faster than the balanced
preset. The README measures stock low-step sampling at 0.556 SSIM against the
reference. The folding code is adapter-agnostic, allowing use with the wider
LoRA ecosystem.
It handles style adapters and sequential folds, and it can be used for future
distillations. On APFS, each folded variant uses ~2-3 GB of real disk. The PR
also contains three small fixes found while running h3 on large real workloads;
I am happy to split them into separate PRs if preferred.

tools/fold_turbo_lora.py

tools/fold_turbo_lora.py applies W' = W + scale*(B@A) to the shards offline.
It clones the original files, using copy-on-write where the filesystem supports
it, and rewrites only the affected byte ranges. The headers and tensor order
remain byte-identical, as does the alignment. The implementation is pure numpy
and runs a per-tensor parity probe before writing any bytes.

For a checkpoint folded with larryvrh's Turbo v4 adapter (Apache-2.0), h3 uses
--steps 5 or 6 without other speed flags. Cold single-shot measurements on
an M5 Max at 960x544 used the same prompt and seed, with the tutorial's balanced
preset (--steps 20 --reuse 2 --layers 45) as the comparison:

balanced preset folded turbo, 6 steps
39 frames 87s 65s
124-frame (5 s) clip 8.8min 6.2min

Visual quality was comparable on the clips tested, although fast action showed
motion smear at 4 steps. The README therefore documents 5 as the floor and warns
against combining the distilled schedule with --reuse or --core-reuse, since
that schedule removes the redundancy used by both options.

Fixes

Relative shader lookup

A relative shader path is now resolved against the executable's directory when
the file is absent from the CWD. Previously, h3 failed with "cannot compile
h3_shaders.metal" unless it was launched from the source directory.

Misaligned safetensors

Files whose data section offset, 8 + header bytes, is not 8-byte aligned are now
rejected with an actionable message. Such files are spec-valid, but they
silently decode to garbage in h3, producing black video with intact audio.
MLX's save_safetensors can write these files because it does not pad the JSON
header. Round-tripping a checkpoint verified that the unpadded file renders
black, while the same file renders correctly after its header is padded to the
next 8-byte boundary without changing tensor order. Accepting these files would
be the proper fix; rejecting them explicitly is a first step.

VAE tile limit

H3_VAE_TILE_PIXELS is now clamped to 320 to match the automatic search. Tiles
above 320 produce a grid/quilt artifact across the whole frame. This was
reproduced at 512 and 1088 on an M5 Max at 960x544.

All measurements were made on a MacBook Pro with an M5 Max 128 GB configuration
running macOS 26.5. The MiniMax H3 Community License's territorial restrictions
apply to the weights and, under its terms, to the adapter as a derivative.

guyz and others added 5 commits August 11, 2026 16:50
A relative h3_shaders.metal that is absent from the current directory now
falls back to the directory containing the binary, so h3 can be launched
from anywhere (PATH installs, daemons, other projects) instead of failing
with 'cannot compile h3_shaders.metal'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GPU path maps tensor data in place and assumes the data section (8 +
header bytes) starts 8-byte aligned. Writers that do not pad the JSON
header (e.g. MLX's save_safetensors) produce spec-valid files that load
without error here but decode to garbage, rendering silent black video
with intact audio. Verified by round-tripping a checkpoint through such a
writer: unpadded renders black, the same file with the header padded to
the next 8-byte boundary renders correctly, tensor order unchanged.

Refuse such files at open with an actionable message rather than
misrendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The env override accepted up to 512, but tiles above 320 produce a
visible grid/quilt artifact across the whole frame (reproduced at 512 and
1088 on M5 Max, 960x544 canvas). Match the automatic search's 320 bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heckpoint

Folds W' = W + scale*(B@A) into the bf16 shards offline by cloning the
originals (copy-on-write where available) and rewriting only the affected
byte ranges, keeping headers, tensor order, and alignment byte-identical.
Pure numpy, with a per-tensor parity probe before any bytes are written.

With larryvrh's Turbo v4 adapter folded, 5-6 step sampling runs with no
LoRA runtime and no speed flags. Measured on M5 Max at 960x544 against
the tutorial's balanced preset (same prompt and seed, cold single-shot):
39 frames 87s -> 65s, 124-frame clip 8.8min -> 6.2min, at comparable
visual quality.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AlexanderIstomin added a commit to AlexanderIstomin/h3.c that referenced this pull request Aug 15, 2026
All four come from pull requests open against antirez/h3.c that have not
been merged yet. Each was applied to this fork, built, and exercised
here rather than taken on trust.

Keep scaled queries in F32 for causal GQA (morluto, PR antirez#4). The kernel
expanded a BF16 query to F32, applied the attention scale, then rounded
the product back to BF16 before the QK contraction — into a threadgroup
float array, so the rounding saved no storage and only discarded
precision. Their regression test, included here, measures max absolute
error 0.0039 and a 0.0076% BF16 mismatch rate on this machine, against
0.092 and 21.6% for the old rounding. This changes the numerics of every
generation, which is why the test ships with it.

Resolve a relative shader path against the executable's directory
(guyz, PR antirez#14). Adapted rather than cherry-picked: upstream patches the
inline path in h3_gpu_create, while this fork resolves paths in
h3_gpu_shader_path, so the fallback lands there and covers the
precompiled metallib too. Without it the binary only runs from the
directory holding h3_shaders.metal.

Clamp H3_VAE_TILE_PIXELS to 320 (guyz, PR antirez#14). Tiles above 320 produce
a grid artifact across the frame. This fork's automatic search already
stopped at 320; only the environment override could exceed it.

Keep video VAE RGB output finite (morluto, PR antirez#9) and reject
unrepresentable aligned frame counts (morluto, PR antirez#11). Both close paths
where a bad value propagates instead of failing: NaN passes two ordered
clamp comparisons untouched, and a large frame count overflows while
being aligned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@monotected

Copy link
Copy Markdown

Hi!
is somehow possible to bake lora with lora_unet_blocks_X.. (mainly from comui projects) ?

@monotected

Copy link
Copy Markdown

hi thanks again for your lora scrit , make a script for bake 2-3 loras from Civitai (seems to work )

#!/bin/bash

Universal LoRA Baking Script for h3.c

Usage: ./bake.sh /path/to/your/new_lora.safetensors

if [ -z "$1" ]; then
echo "Error: Please provide the path to the LoRA file!"
echo "Example: ./bake.sh /Users/icoidd/11/lora100.safetensors"
exit 1
fi

LORA_PATH=$1
LORA_NAME=$(basename "$LORA_PATH")

echo "================================================="
echo "Starting bake for $LORA_NAME"
echo "================================================="

1. Clean and rename keys to h3.c format

echo "[1/5] Cleaning and renaming keys..."
python3 -c "
from safetensors.torch import load_file, save_file
import re

tensors = load_file('$LORA_PATH', device='cpu')
new_tensors = {}

Pattern 1 (lora_unet_blocks_ style)

p1 = re.compile(r'^lora_unet_blocks_(\d+)([a-z]+)(.+).lora_(down|up).weight$')

Pattern 2 (diffusion_model.blocks style)

p2 = re.compile(r'^diffusion_model.blocks_(\d+)([a-z]+)(.+).lora_(A|B).weight$')

for k, v in tensors.items():
if 'alpha' in k: continue
m = p1.match(k)
if m:
new_k = f"blocks.{m.group(1)}.{m.group(2)}.{m.group(3)}.{'lora_A' if m.group(4)=='down' else 'lora_B'}.weight"
new_tensors[new_k] = v
continue
m = p2.match(k)
if m:
new_k = f"blocks.{m.group(1)}.{m.group(2)}.{m.group(3)}.lora_{m.group(4)}.weight"
new_tensors[new_k] = v

if not new_tensors:
print('ERROR: No matching LoRA keys found! Check architecture.')
exit(1)

save_file(new_tensors, 'temp_lora_cleaned.safetensors')
print(f'Cleaned layers: {len(new_tensors)}')
"

2. Bake FL2VA (Base -> Custom LoRA -> Turbo)

echo "[2/5] Baking FL2VA (Base -> Custom LoRA -> Turbo)..."
rm -rf ./MiniMax-CustomLoRA-fl2va
python3 tools/fold_turbo_lora.py --checkpoint ./MiniMax-H3/FL2VA/transformer --lora ./temp_lora_cleaned.safetensors --out ./temp_bake_1
python3 tools/fold_turbo_lora.py --checkpoint ./temp_bake_1 --lora ./minimax_h3_turbo_v4_step600_ema.safetensors --out ./MiniMax-CustomLoRA-fl2va/FL2VA/transformer
rm -rf ./temp_bake_1

3. Bake Ref2VA

echo "[3/5] Baking Ref2VA (Base -> Custom LoRA -> Turbo)..."
rm -rf ./MiniMax-CustomLoRA-ref2va
python3 tools/fold_turbo_lora.py --checkpoint ./MiniMax-H3/Ref2VA/transformer --lora ./temp_lora_cleaned.safetensors --out ./temp_bake_1
python3 tools/fold_turbo_lora.py --checkpoint ./temp_bake_1 --lora ./minimax_h3_turbo_v4_step600_ema.safetensors --out ./MiniMax-CustomLoRA-ref2va/Ref2VA/transformer
rm -rf ./temp_bake_1

4. Remove temp file

rm -f ./temp_lora_cleaned.safetensors

5. Update Master folder

echo "[4/5] Updating Master folder..."
rm -f ./MiniMax-DoubleLoRA-Master/FL2VA/transformer
rm -f ./MiniMax-DoubleLoRA-Master/Ref2VA/transformer
ln -s ../../MiniMax-CustomLoRA-fl2va/FL2VA/transformer ./MiniMax-DoubleLoRA-Master/FL2VA/transformer
ln -s ../../MiniMax-CustomLoRA-ref2va/Ref2VA/transformer ./MiniMax-DoubleLoRA-Master/Ref2VA/transformer

echo "[5/5] DONE! Model updated."
echo "Run: ./h3 -d ./MiniMax-DoubleLoRA-Master -p ..."

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