honeydo 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ """turbo LoRA -> int8 DiT bundle 合并器(Lightning 加速档的一次性离线准备)。
2
+
3
+ RH-RunningHub/MiniMax-H3-MultiGPU-Lightning 的加速实践 = 步数蒸馏 + 注意力/缓存
4
+ 优化 + 多卡并行;其中能在 Apple Silicon 落地的是**步数蒸馏**:官方蒸馏权重未公开,
5
+ 社区开源替代是 lightx2v/Minimax-h3-Turbo 等蒸馏 LoRA(详见 USAGE.md 视频块)。
6
+ 本脚本把蒸馏 LoRA 按 `W += scale * B @ A` 合并进 mmh3turbo 的 per-row int8
7
+ bundle,产物用 `--weights` 直接加载(或 `lmedia video gen --fast` 自动走 dit-turbo)。
8
+
9
+ 设计约束(都在 defend 正确性,宁可拒绝也不静默错配):
10
+ - 只支持 lightx2v/Minimax-h3-Turbo 命名(transformer_blocks.*.attn.to_q / to_k /
11
+ to_v / to_out.0 / ff.net.0.proj / ff.net.2)。larryvrh 命名含全宽 adaLN delta,
12
+ 对 pruned 基座(8 维曲线基座 adaLN)需要重建时间嵌入,不支持。
13
+ - 源 bundle 必须无 SmoothQuant 校准(idx 无 L*.dout / L*.dw2)。带校准的 bundle
14
+ 里 out/fc2 权重按输入通道除数缩放、qkv/fc1 除数混在 norm 权重里无法恢复,
15
+ 直接合并 delta 是错的 -> 显式报错。
16
+ - token_refiner 的 delta 跳过:lightx2v refiner 输入维 5376 != 本引擎 refiner 的
17
+ 5120(cond_dim),形状对不上;refiner 只精炼文本条件,跳过影响很小。
18
+ - 合并后按输出行重新对称量化(sym8),量化误差 relL2 写进 idx __meta__ 供审计。
19
+
20
+ 纯 numpy 实现(不 import mmh3turbo):张量形状全部取自 idx/实张量,因此可用
21
+ 合成小 bundle 做单元测试,也顺带兼容未来维度变化。
22
+ """
23
+ import argparse
24
+ import json
25
+ import os
26
+ import re
27
+ import struct
28
+ import sys
29
+
30
+ import numpy as np
31
+
32
+ _NP = {"F32": np.float32, "F16": np.float16, "U8": np.uint8, "I8": np.int8,
33
+ "I32": np.int32, "I64": np.int64, "BOOL": np.bool_}
34
+
35
+
36
+ class Safetensors:
37
+ """最小懒加载 reader;BF16 numpy 不认识,手动升 fp32(convert.py 同款)。"""
38
+
39
+ def __init__(self, path):
40
+ self.path = str(path)
41
+ with open(self.path, "rb") as f:
42
+ n = struct.unpack("<Q", f.read(8))[0]
43
+ self.header = json.loads(f.read(n))
44
+ self.header.pop("__metadata__", None)
45
+ self.base = 8 + n
46
+ self.mm = np.memmap(self.path, dtype=np.uint8, mode="r")
47
+
48
+ def keys(self):
49
+ return self.header.keys()
50
+
51
+ def get(self, k):
52
+ e = self.header[k]
53
+ s, t = e["data_offsets"]
54
+ raw = self.mm[self.base + s:self.base + t]
55
+ d = e["dtype"]
56
+ shape = tuple(e["shape"])
57
+ if d == "BF16":
58
+ u = raw.copy().view(np.uint16).astype(np.uint32) << 16
59
+ a = u.view(np.float32)
60
+ elif d in _NP:
61
+ a = raw.copy().view(_NP[d])
62
+ else:
63
+ raise ValueError(f"不支持的 dtype {d}({k})")
64
+ return a.reshape(shape)
65
+
66
+
67
+ class Writer:
68
+ """扁平 blob + JSON index,与引擎 Model.load 期望的布局一致(convert.py 同款)。"""
69
+
70
+ def __init__(self, path):
71
+ self.f = open(path, "wb")
72
+ self.idx = {}
73
+
74
+ def put(self, name, arr, dtype):
75
+ a = np.ascontiguousarray(np.asarray(arr).astype(dtype))
76
+ off = self.f.tell()
77
+ pad = (-off) % 64
78
+ if pad:
79
+ self.f.write(b"\0" * pad)
80
+ off += pad
81
+ self.f.write(a.tobytes())
82
+ self.idx[name] = {"off": off, "len": a.nbytes,
83
+ "dt": np.dtype(dtype).name, "shape": list(a.shape)}
84
+
85
+ def put_raw(self, name, raw, dt, shape):
86
+ """未改动的张量按原字节拷贝(dtype/shape 来自源 idx,不重解释)。"""
87
+ off = self.f.tell()
88
+ pad = (-off) % 64
89
+ if pad:
90
+ self.f.write(b"\0" * pad)
91
+ off += pad
92
+ self.f.write(raw)
93
+ self.idx[name] = {"off": off, "len": len(raw),
94
+ "dt": dt, "shape": list(shape)}
95
+
96
+ def close(self, meta):
97
+ self.idx["__meta__"] = meta
98
+ self.f.close()
99
+
100
+
101
+ def sym8(w):
102
+ """per-output-row 对称 int8。w:[N,K] -> (int8[N,K], f32[N])(convert.py 同款)。"""
103
+ amax = np.abs(w).max(axis=1).astype(np.float32)
104
+ s = np.where(amax > 0, amax / 127.0, np.float32(1.0))
105
+ q = np.rint(w / s[:, None]).clip(-127, 127).astype(np.int8)
106
+ return q, s
107
+
108
+
109
+ # lightx2v 命名 -> bundle GEMM 标签;qkv 三个 delta 按行拼回 fused 权重
110
+ LORA_TAG = {
111
+ "attn.to_q": "qkv", "attn.to_k": "qkv", "attn.to_v": "qkv",
112
+ "attn.to_out.0": "out",
113
+ "ff.net.0.proj": "fc1",
114
+ "ff.net.2": "fc2",
115
+ }
116
+ TAG_TO_TAILS = {g: [t for t, gg in LORA_TAG.items() if gg == g]
117
+ for g in set(LORA_TAG.values())}
118
+
119
+
120
+ def parse_lora_keys(keys):
121
+ """-> {block_index: {lora_tail: {'A': key, 'B': key}}},无法归类的 key 原样返回。"""
122
+ pat = re.compile(r"^(.*?)\.lora_([AB])(?:\.\w+)?\.weight$")
123
+ parsed, unknown = {}, []
124
+ for k in keys:
125
+ m = pat.match(k)
126
+ if not m:
127
+ unknown.append(k)
128
+ continue
129
+ base, ab = m.group(1), m.group(2)
130
+ block = None
131
+ bm = re.match(r"^transformer_blocks\.(\d+)\.(.+)$", base)
132
+ if bm:
133
+ block, tail = int(bm.group(1)), bm.group(2)
134
+ elif base.startswith("token_refiner."):
135
+ continue # shape 不兼容,见模块 docstring
136
+ else:
137
+ unknown.append(k)
138
+ continue
139
+ if tail not in LORA_TAG:
140
+ unknown.append(k)
141
+ continue
142
+ entry = parsed.setdefault(block, {}).setdefault(tail, {})
143
+ entry[ab] = k
144
+ return parsed, unknown
145
+
146
+
147
+ def merge(lora_path, bundle, out, scale=1.0, nfe=None):
148
+ idx = json.load(open(bundle + ".idx"))
149
+ meta = idx.pop("__meta__", {})
150
+ for i in range(int(meta.get("n_blocks", 0))):
151
+ if f"L{i}.dout" in idx or f"L{i}.dw2" in idx:
152
+ sys.exit(
153
+ f"源 bundle 带 SmoothQuant 校准(L{i}.dout/dw2 存在):delta 需要"
154
+ f"按除数缩放,本实现不支持。请换用未校准 bundle(mmh3turbo-bundles)。")
155
+ blob = np.memmap(bundle + ".bin", dtype=np.uint8, mode="r")
156
+ lora = Safetensors(lora_path)
157
+ parsed, unknown = parse_lora_keys(lora.keys())
158
+ if not parsed:
159
+ sys.exit("LoRA 里没有可识别的 transformer_blocks.* delta"
160
+ f"(只支持 lightx2v 命名);未识别 key 例: {unknown[:3]}")
161
+
162
+ def read_bundle(name):
163
+ r = idx[name]
164
+ return np.frombuffer(blob[r["off"]:r["off"] + r["len"]].tobytes(),
165
+ dtype=np.dtype(r["dt"])).reshape(r["shape"])
166
+
167
+ w = Writer(out + ".bin")
168
+ errs, merged_list = [], []
169
+ for name in idx:
170
+ r = idx[name]
171
+ m = re.match(r"^L(\d+)\.(qkv|out|fc1|fc2)\.(w|s)$", name)
172
+ if not (m and int(m.group(1)) in parsed):
173
+ w.put_raw(name, blob[r["off"]:r["off"] + r["len"]].tobytes(),
174
+ r["dt"], r["shape"])
175
+ continue
176
+ i, tag, part = int(m.group(1)), m.group(2), m.group(3)
177
+ if part == "s":
178
+ continue # 已随同名 .w 一起写出
179
+ block_lora = parsed[i]
180
+ missing = [t for t in TAG_TO_TAILS[tag] if t not in block_lora]
181
+ if missing:
182
+ sys.exit(f"L{i}.{tag}: LoRA 缺少 {missing} 的 delta,拒绝部分合并")
183
+ w8 = read_bundle(f"L{i}.{tag}.w").astype(np.float32)
184
+ s = read_bundle(f"L{i}.{tag}.s").astype(np.float32)
185
+ target = w8 * s[:, None]
186
+ deltas = []
187
+ for tail in TAG_TO_TAILS[tag]:
188
+ a = lora.get(block_lora[tail]["A"]).astype(np.float32)
189
+ b = lora.get(block_lora[tail]["B"]).astype(np.float32)
190
+ deltas.append(b @ a)
191
+ delta = np.concatenate(deltas, axis=0) if len(deltas) > 1 else deltas[0]
192
+ if delta.shape != target.shape:
193
+ sys.exit(f"L{i}.{tag}: delta {delta.shape} 与 bundle 权重 {target.shape} "
194
+ f"形状不符——LoRA 与该 bundle 不是同一架构,拒绝合并")
195
+ target = target + scale * delta
196
+ q, s_new = sym8(target)
197
+ rec = q.astype(np.float32) * s_new[:, None]
198
+ errs.append(float(np.linalg.norm(rec - target) / np.linalg.norm(target)))
199
+ w.put(f"L{i}.{tag}.w", q, np.int8)
200
+ w.put(f"L{i}.{tag}.s", s_new, np.float32)
201
+ merged_list.append(f"L{i}.{tag}")
202
+
203
+ for name in merged_list:
204
+ print(f" merged {name}", flush=True)
205
+ rel = {"mean": float(np.mean(errs)), "max": float(np.max(errs))} if errs \
206
+ else {"mean": 0.0, "max": 0.0}
207
+ meta.update({"turbo_lora": os.path.basename(lora_path), "turbo_scale": scale,
208
+ "turbo_relL2": rel, "turbo_merged": len(merged_list)})
209
+ if nfe is not None:
210
+ meta["turbo_nfe"] = nfe
211
+ w.close(meta)
212
+ json.dump(w.idx, open(out + ".idx", "w"))
213
+ print(f"wrote {out}.bin {os.path.getsize(out + '.bin') / 2**30:.2f} GiB, "
214
+ f"{len(merged_list)} GEMM 已合并(relL2 mean {rel['mean']:.5f} max {rel['max']:.5f})")
215
+ if unknown:
216
+ print(f" [未消费 LoRA key x{len(unknown)}] {unknown[:4]}(refiner/未支持命名,已跳过)")
217
+
218
+
219
+ def main():
220
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
221
+ ap.add_argument("--lora", required=True, help="蒸馏 LoRA safetensors(lightx2v 命名)")
222
+ ap.add_argument("--bundle", default=os.path.expanduser("~/.cache/mmh3turbo/dit"),
223
+ help="源 int8 bundle 前缀(dit.bin/dit.idx)")
224
+ ap.add_argument("--out", default=os.path.expanduser("~/.cache/mmh3turbo/dit-turbo"),
225
+ help="输出 bundle 前缀")
226
+ ap.add_argument("--scale", type=float, default=1.0,
227
+ help="LoRA 强度(蒸馏配方固定 1.0)")
228
+ ap.add_argument("--nfe", type=int, default=None,
229
+ help="该蒸馏 LoRA 的推荐去噪步数(写入 bundle meta,gen --fast 读它做默认步数)")
230
+ a = ap.parse_args()
231
+ if not os.path.exists(a.bundle + ".bin") or not os.path.exists(a.bundle + ".idx"):
232
+ sys.exit(f"源 bundle 不存在: {a.bundle}.bin/.idx(先 lmedia video setup)")
233
+ if not os.path.exists(a.lora):
234
+ sys.exit(f"LoRA 文件不存在: {a.lora}")
235
+ merge(a.lora, a.bundle, a.out, a.scale, a.nfe)
236
+
237
+
238
+ if __name__ == "__main__":
239
+ main()
@@ -0,0 +1,31 @@
1
+ """lmedia video gen --fast 的驱动:few-step 蒸馏 bundle + 蒸馏训练用的 flow shift。
2
+
3
+ Lightning 方案(RH-RunningHub/MiniMax-H3-MultiGPU-Lightning)的本地适配里,
4
+ 步数蒸馏的推理侧唯一要改的是 sigma 调度的 video shift:v1.x 768p 系蒸馏 LoRA
5
+ 用 shift 6/3 训练(ModelTC Minimax-h3-Turbo specs),而 mmh3turbo 为基座硬编码
6
+ 12/3。引擎把 SHIFT_VIDEO 以 def 期默认值(sigmas / timestep_classes)与模块全局
7
+ (denoise 的 time_shift_slope 调用)散布在两处,这里统一打补丁后透传给
8
+ mmh3turbo.generate.main——产物协议(目录内 video.mp4)与原 CLI 完全一致。
9
+ """
10
+ import argparse
11
+ import sys
12
+
13
+ _ap = argparse.ArgumentParser(add_help=False)
14
+ _ap.add_argument("--shift-video", type=float, default=6.0,
15
+ help="蒸馏 LoRA 训练用的 video flow shift(v1.x 768p 系=6;基座=12)")
16
+ known, rest = _ap.parse_known_args()
17
+
18
+ import mmh3turbo.sampler as _sampler # noqa: E402 (patch 必须在 import 主流程前)
19
+ import mmh3turbo.denoise as _denoise # noqa: E402
20
+
21
+ _sampler.SHIFT_VIDEO = _denoise.SHIFT_VIDEO = known.shift_video
22
+ _sampler.sigmas.__defaults__ = (known.shift_video,)
23
+ _sampler.timestep_classes.__defaults__ = (
24
+ known.shift_video, _sampler.SHIFT_AUDIO, False, _sampler.VISUAL_COND_TIMESTEP)
25
+
26
+ from mmh3turbo.generate import main # noqa: E402
27
+
28
+ if not rest:
29
+ sys.exit("缺少生成参数(prompt/-r/--steps/...),用法与 mmh3turbo 相同")
30
+ sys.argv = ["mmh3turbo", *rest]
31
+ main()
File without changes