Remove conversion tools from model repository
Browse files- NOTICE +2 -2
- README.md +2 -4
- requirements.txt +0 -3
- tools/README.md +0 -49
- tools/convert_int8_convrot.py +0 -556
- tools/mm_profile_main_quant.py +0 -143
- tools/mm_quantize_lean.py +0 -562
- tools/mm_validate_dynamic_built.py +0 -137
NOTICE
CHANGED
|
@@ -6,5 +6,5 @@ The FL2VA safetensors checkpoint in this repository is a modified derivative of
|
|
| 6 |
MiniMax H3. It was converted from the original Diffusers tensor layout to the
|
| 7 |
ComfyUI layout, selectively quantized to INT8 ConvRot, and compressed with a
|
| 8 |
dynamic rank-16 representation of the AdaLN time-conditioning curve. The
|
| 9 |
-
included ComfyUI patch
|
| 10 |
-
|
|
|
|
| 6 |
MiniMax H3. It was converted from the original Diffusers tensor layout to the
|
| 7 |
ComfyUI layout, selectively quantized to INT8 ConvRot, and compressed with a
|
| 8 |
dynamic rank-16 representation of the AdaLN time-conditioning curve. The
|
| 9 |
+
included ComfyUI patch is also a modified file provided for this derivative
|
| 10 |
+
release.
|
README.md
CHANGED
|
@@ -36,7 +36,6 @@ small ComfyUI core patch included in [`comfy_patch/`](comfy_patch/).
|
|
| 36 |
| [`reports/layer_policy.json`](reports/layer_policy.json) | Exact per-layer precision policy |
|
| 37 |
| [`reports/validation.json`](reports/validation.json) | Structural, numerical, and CPU-load results |
|
| 38 |
| [`reports/mm_quant_profile_fl2va.json`](reports/mm_quant_profile_fl2va.json) | Row-sampled reconstruction profile for all 200 main matrices |
|
| 39 |
-
| [`tools/`](tools/) | Audited converter, profiler, and validator sources |
|
| 40 |
|
| 41 |
This repository intentionally does **not** include the MiniMax-H3 text encoder,
|
| 42 |
tokenizer, or video/audio VAEs. The text encoder is being prepared as a separate
|
|
@@ -168,15 +167,14 @@ Therefore, 20.999 GiB is the on-disk tensor payload, not a promise that every
|
|
| 168 |
workflow will remain under 24 GiB. Activations, runtime buffers, resolution,
|
| 169 |
frame count, batch size, and offloading policy determine peak VRAM.
|
| 170 |
|
| 171 |
-
##
|
| 172 |
|
| 173 |
The converter reads the original top-level `transformer/` Diffusers shards from
|
| 174 |
MiniMaxAI directly; it does not create a 61+ GiB merged BF16 intermediate. The
|
| 175 |
nested task-specific transformer folder was not used because its already-packed
|
| 176 |
QKV is head-major, while current ComfyUI expects global `cat(Q,K,V)` packing.
|
| 177 |
|
| 178 |
-
|
| 179 |
-
validation commands. The official
|
| 180 |
[`minimax_h3_fl2va_pruned_int8_convrot.safetensors`](https://huggingface.co/Comfy-Org/MiniMax-H3/blob/main/diffusion_models/minimax_h3_fl2va_pruned_int8_convrot.safetensors)
|
| 181 |
was used only as a layout reference and as the source of the FP32
|
| 182 |
`rope.inv_freq` tensor absent from the raw Diffusers state dict.
|
|
|
|
| 36 |
| [`reports/layer_policy.json`](reports/layer_policy.json) | Exact per-layer precision policy |
|
| 37 |
| [`reports/validation.json`](reports/validation.json) | Structural, numerical, and CPU-load results |
|
| 38 |
| [`reports/mm_quant_profile_fl2va.json`](reports/mm_quant_profile_fl2va.json) | Row-sampled reconstruction profile for all 200 main matrices |
|
|
|
|
| 39 |
|
| 40 |
This repository intentionally does **not** include the MiniMax-H3 text encoder,
|
| 41 |
tokenizer, or video/audio VAEs. The text encoder is being prepared as a separate
|
|
|
|
| 167 |
workflow will remain under 24 GiB. Activations, runtime buffers, resolution,
|
| 168 |
frame count, batch size, and offloading policy determine peak VRAM.
|
| 169 |
|
| 170 |
+
## Conversion provenance
|
| 171 |
|
| 172 |
The converter reads the original top-level `transformer/` Diffusers shards from
|
| 173 |
MiniMaxAI directly; it does not create a 61+ GiB merged BF16 intermediate. The
|
| 174 |
nested task-specific transformer folder was not used because its already-packed
|
| 175 |
QKV is head-major, while current ComfyUI expects global `cat(Q,K,V)` packing.
|
| 176 |
|
| 177 |
+
The official
|
|
|
|
| 178 |
[`minimax_h3_fl2va_pruned_int8_convrot.safetensors`](https://huggingface.co/Comfy-Org/MiniMax-H3/blob/main/diffusion_models/minimax_h3_fl2va_pruned_int8_convrot.safetensors)
|
| 179 |
was used only as a layout reference and as the source of the FP32
|
| 180 |
`rope.inv_freq` tensor absent from the raw Diffusers state dict.
|
requirements.txt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
torch
|
| 2 |
-
safetensors
|
| 3 |
-
comfy-kitchen
|
|
|
|
|
|
|
|
|
|
|
|
tools/README.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
| 1 |
-
# Reproduction tools
|
| 2 |
-
|
| 3 |
-
These are the exact converter, profiler, and validator used for this release,
|
| 4 |
-
with local paths replaced by CLI arguments. They are research utilities, not a
|
| 5 |
-
general MiniMax-H3 conversion library.
|
| 6 |
-
|
| 7 |
-
Requirements:
|
| 8 |
-
|
| 9 |
-
- Python 3.10+
|
| 10 |
-
- PyTorch
|
| 11 |
-
- `safetensors`
|
| 12 |
-
- `comfy-kitchen` from a current ComfyUI environment
|
| 13 |
-
|
| 14 |
-
The converter reads the original top-level Diffusers shards directly; it does
|
| 15 |
-
not need a merged BF16 file. Use `MiniMaxAI/MiniMax-H3`'s `transformer/` folder,
|
| 16 |
-
not the nested task folder whose already-packed QKV uses a different row order.
|
| 17 |
-
It also uses the official pruned ComfyOrg file as a structural reference and to
|
| 18 |
-
copy `rope.inv_freq`, which is absent from the raw Diffusers state dict.
|
| 19 |
-
|
| 20 |
-
Dry run:
|
| 21 |
-
|
| 22 |
-
```bash
|
| 23 |
-
python tools/mm_quantize_lean.py fl2va \
|
| 24 |
-
--src /path/to/MiniMax-H3/transformer \
|
| 25 |
-
--reference /path/to/minimax_h3_fl2va_pruned_int8_convrot.safetensors \
|
| 26 |
-
--dst /path/to/output.safetensors \
|
| 27 |
-
--profile quality21 --time-mode dynamic --rank 16 --device cpu --dry-run
|
| 28 |
-
```
|
| 29 |
-
|
| 30 |
-
Build on CPU:
|
| 31 |
-
|
| 32 |
-
```bash
|
| 33 |
-
python tools/mm_quantize_lean.py fl2va \
|
| 34 |
-
--src /path/to/MiniMax-H3/transformer \
|
| 35 |
-
--reference /path/to/minimax_h3_fl2va_pruned_int8_convrot.safetensors \
|
| 36 |
-
--dst /path/to/output.safetensors \
|
| 37 |
-
--profile quality21 --time-mode dynamic --rank 16 --device cpu --overwrite
|
| 38 |
-
```
|
| 39 |
-
|
| 40 |
-
Validate:
|
| 41 |
-
|
| 42 |
-
```bash
|
| 43 |
-
python tools/mm_validate_dynamic_built.py fl2va /path/to/output.safetensors \
|
| 44 |
-
--src /path/to/MiniMax-H3/transformer --profile quality21
|
| 45 |
-
```
|
| 46 |
-
|
| 47 |
-
The public scripts preserve the exact mapping and quantization behavior used by
|
| 48 |
-
the release. Some diagnostic messages remain in Russian because these files are
|
| 49 |
-
the audited build sources rather than a rewritten approximation.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/convert_int8_convrot.py
DELETED
|
@@ -1,556 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
r"""Конвертер моделей ComfyUI в INT8 ConvRot.
|
| 3 |
-
|
| 4 |
-
Берёт safetensors (bf16/fp16/fp32) и квантует линейные слои в int8 с поворотом
|
| 5 |
-
Адамара, как это делают официальные сборки Comfy-Org (int8_tensorwise + convrot).
|
| 6 |
-
|
| 7 |
-
Не трогает: нормализации, эмбеддеры, bias, 1D-тензоры и матрицы, у которых
|
| 8 |
-
входная размерность не кратна размеру группы — они остаются в исходной точности.
|
| 9 |
-
|
| 10 |
-
Запуск:
|
| 11 |
-
python_embeded\python.exe -s tools\convert_int8_convrot.py <вход.safetensors> <выход.safetensors>
|
| 12 |
-
[--groupsize 256] [--device cuda:0] [--dry-run]
|
| 13 |
-
"""
|
| 14 |
-
import argparse
|
| 15 |
-
import json
|
| 16 |
-
import os
|
| 17 |
-
import re
|
| 18 |
-
import sys
|
| 19 |
-
import time
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
from safetensors import safe_open
|
| 23 |
-
from safetensors.torch import save_file
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
class HFShardReader:
|
| 27 |
-
"""Читает HF-репозиторий (папку с шардами) так же, как safe_open один файл.
|
| 28 |
-
|
| 29 |
-
Нужно, чтобы квантовать прямо из оригинала, не складывая на диск
|
| 30 |
-
промежуточный файл на десятки гигабайт. Значения при этом те же самые:
|
| 31 |
-
склейка ничего не меняла, но лишняя запись и чтение никому не нужны.
|
| 32 |
-
|
| 33 |
-
Имена приводятся к раскладке ComfyUI теми же правилами, что в
|
| 34 |
-
merge_hf_to_comfy.py, визуальная часть остаётся на месте.
|
| 35 |
-
"""
|
| 36 |
-
|
| 37 |
-
RENAME = [
|
| 38 |
-
("model.language_model.", "model."),
|
| 39 |
-
("language_model.model.", "model."),
|
| 40 |
-
("language_model.", ""),
|
| 41 |
-
("vision_tower.", ""),
|
| 42 |
-
]
|
| 43 |
-
|
| 44 |
-
def __init__(self, path: str):
|
| 45 |
-
import glob
|
| 46 |
-
self._files = {}
|
| 47 |
-
self._map = {}
|
| 48 |
-
shards = sorted(glob.glob(os.path.join(path, "*.safetensors")))
|
| 49 |
-
if not shards:
|
| 50 |
-
raise SystemExit(f"в папке нет файлов safetensors: {path}")
|
| 51 |
-
for shard in shards:
|
| 52 |
-
f = safe_open(shard, framework="pt")
|
| 53 |
-
self._files[shard] = f
|
| 54 |
-
for k in f.keys():
|
| 55 |
-
self._map[self._rename(k)] = (shard, k)
|
| 56 |
-
print(f" прочитано шардов: {len(shards)}, тензоров: {len(self._map)}")
|
| 57 |
-
|
| 58 |
-
@classmethod
|
| 59 |
-
def _rename(cls, key: str) -> str:
|
| 60 |
-
for old, new in cls.RENAME:
|
| 61 |
-
if key.startswith(old):
|
| 62 |
-
rest = key[len(old):]
|
| 63 |
-
return rest if rest.startswith("model.") or not new else new + rest
|
| 64 |
-
return key
|
| 65 |
-
|
| 66 |
-
def keys(self):
|
| 67 |
-
return list(self._map)
|
| 68 |
-
|
| 69 |
-
def get_tensor(self, key: str):
|
| 70 |
-
shard, orig = self._map[key]
|
| 71 |
-
return self._files[shard].get_tensor(orig)
|
| 72 |
-
|
| 73 |
-
def metadata(self):
|
| 74 |
-
return {"format": "pt"}
|
| 75 |
-
|
| 76 |
-
def __enter__(self):
|
| 77 |
-
return self
|
| 78 |
-
|
| 79 |
-
def __exit__(self, *exc):
|
| 80 |
-
self._files.clear()
|
| 81 |
-
|
| 82 |
-
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "ComfyUI"))
|
| 83 |
-
|
| 84 |
-
# Слои, которые официальные сборки Comfy-Org оставляют в исходной точности:
|
| 85 |
-
# нормализации, эмбеддеры, входной/выходной слой. Их квантование заметно бьёт по качеству.
|
| 86 |
-
SKIP_TOKENS = (
|
| 87 |
-
# диффузионные трансформеры
|
| 88 |
-
# "_emb." с точкой, а не "_emb": иначе под запрет попадают модули вида
|
| 89 |
-
# video_embeddings_connector - а это обычные блоки внимания и полносвязные
|
| 90 |
-
# слои на 4 ГБ, которые квантовать можно и нужно
|
| 91 |
-
"norm", "embedder", "pad_token", "emb.", "_emb.",
|
| 92 |
-
"final_layer", "x_embedder", "proj_out", "time_in", "vector_in",
|
| 93 |
-
# Слои модуляции: выдают масштаб и сдвиг для каждого блока, поэтому ошибка
|
| 94 |
-
# в них умножается на всю сеть. Мы их не квантуем НИКОГДА - это наше решение
|
| 95 |
-
# по замеру, а не подражание чужим сборкам.
|
| 96 |
-
#
|
| 97 |
-
# Единодушия у авторов тут нет, пересчитано по файлам на диске 31.07.2026:
|
| 98 |
-
# LTX 2.3 0 из 24 квантовано
|
| 99 |
-
# Anima 0 из 170
|
| 100 |
-
# Z-Image 32 из 33 <- квантует, вопреки прежней записи в этом коде
|
| 101 |
-
# Поэтому по Z-Image и Ideogram сверка audit_reference_int8.py всегда будет
|
| 102 |
-
# показывать ~84 %, и это ОСОЗНАННОЕ расхождение, а не промах правила.
|
| 103 |
-
"adaln", "modulation",
|
| 104 |
-
# входные проекции (diffusers-нейминг): в эталонных сборках Comfy-Org,
|
| 105 |
-
# obsxrver и supermind они всегда остаются в исходной точности - проверено
|
| 106 |
-
# по файлам LTX 2.3, Z-Image, Krea 2 и Qwen-Image
|
| 107 |
-
"img_in", "txt_in", "patch_embed", "context_embedder",
|
| 108 |
-
# языковые модели (текст-энкодеры)
|
| 109 |
-
"lm_head", "embed_tokens", "token_embd", "shared.", "wte", "wpe",
|
| 110 |
-
# части полных чекпоинтов, которые квантовать нельзя: VAE свёрточный и к
|
| 111 |
-
# int8 чувствителен, текстовая проекция маленькая и стоит на входе, а
|
| 112 |
-
# per_channel_statistics - это вообще не веса, а нормировочные константы
|
| 113 |
-
"vae.", "text_embedding_projection", "per_channel_statistics", "vocoder.",
|
| 114 |
-
)
|
| 115 |
-
|
| 116 |
-
# Коэффициенты подрезания выбросов при подборе масштаба.
|
| 117 |
-
# У диффузионных моделей оптимум почти всегда в 0.95-1.00 (медиана 0.982), поэтому
|
| 118 |
-
# основная часть сетки плотная: шаг 0.001 в диапазоне 1.000-0.940. Но у текст-энкодеров
|
| 119 |
-
# хвосты тяжелее: у qwen2.5-VL 3.2% строк упирались в край 0.940, то есть их оптимум
|
| 120 |
-
# лежал ещё ниже и просто не находился. Поэтому дальше идёт разреженный хвост до 0.80 -
|
| 121 |
-
# он почти не стоит времени, зато снимает обрезание перебора. См. tools/check_range.py.
|
| 122 |
-
def build_ratios(lo: float = 0.80) -> tuple:
|
| 123 |
-
"""Сетка от 1.0 вниз до lo: плотно у единицы, дальше всё разреженнее."""
|
| 124 |
-
out = [round(1.0 - i * 0.001, 4) for i in range(61)] # 1.000 .. 0.940
|
| 125 |
-
out += [round(0.938 - i * 0.002, 4) for i in range(20)] # 0.938 .. 0.900
|
| 126 |
-
out += [round(0.895 - i * 0.005, 4) for i in range(20)] # 0.895 .. 0.800
|
| 127 |
-
return tuple(r for r in out if r >= lo - 1e-9)
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
SEARCH_RATIOS = build_ratios()
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
# Поворот Адамара строится удвоением матрицы 4x4, поэтому размер группы обязан
|
| 134 |
-
# быть степенью четвёрки (см. _build_hadamard: "Regular Hadamard size must be a
|
| 135 |
-
# power of 4"). Отсюда и лесенка: 256 -> 64 -> 16.
|
| 136 |
-
GROUPSIZE_LADDER = (256, 64, 16)
|
| 137 |
-
|
| 138 |
-
_DIGITS = re.compile(r"\d+")
|
| 139 |
-
# Порог повторяемости. Два - это ещё не стопка: linear_fc1/linear_fc2 одного
|
| 140 |
-
# модуля дают ровно два совпадения по шаблону.
|
| 141 |
-
REPEAT_MIN = 3
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def block_patterns(layer: str) -> tuple[str, ...]:
|
| 145 |
-
"""Варианты имени, где обезличена ровно одна числовая позиция.
|
| 146 |
-
|
| 147 |
-
Одновременная замена всех цифр смешивала независимые роли. Например,
|
| 148 |
-
``blocks.0.mlp.fc1`` и ``blocks.1.mlp.fc2`` превращались в один шаблон.
|
| 149 |
-
Из-за этого два блока token_refiner с fc1/fc2 давали ложную стопку из
|
| 150 |
-
четырёх слоёв и проходили порог повторяемости 3.
|
| 151 |
-
"""
|
| 152 |
-
return tuple(layer[:m.start()] + "#" + layer[m.end():] for m in _DIGITS.finditer(layer))
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def repeated_layers(layer_names, repeat_min: int = REPEAT_MIN) -> set:
|
| 156 |
-
"""Слои, которые входят в повторяющуюся стопку блоков.
|
| 157 |
-
|
| 158 |
-
Зачем это вместо списка подстрок. Эталонные сборки квантуют тело сети и
|
| 159 |
-
оставляют в исходной точности края: входные проекции, выходные головы,
|
| 160 |
-
таблицы вложений, мост зрение->язык. Объединяет их не название, а то, что
|
| 161 |
-
они существуют в единственном экземпляре, тогда как тело - это одна и та же
|
| 162 |
-
ноd, повторённая по числу блоков. Номер блока в имени и есть тот признак,
|
| 163 |
-
который отличает тело от края, и он не зависит от того, как автор модели
|
| 164 |
-
назвал свои модули.
|
| 165 |
-
|
| 166 |
-
Проверено сверкой с эталонами (tools/audit_reference_int8.py): по этому
|
| 167 |
-
признаку сами собой отсеиваются lm_head, embed_tokens, patchify_proj,
|
| 168 |
-
visual.merger, visual.pos_embed, adaln_single.emb.* - всё то, что раньше
|
| 169 |
-
приходилось перечислять руками.
|
| 170 |
-
"""
|
| 171 |
-
counts: dict[str, int] = {}
|
| 172 |
-
for name in layer_names:
|
| 173 |
-
for pattern in set(block_patterns(name)):
|
| 174 |
-
counts[pattern] = counts.get(pattern, 0) + 1
|
| 175 |
-
return {
|
| 176 |
-
name for name in layer_names
|
| 177 |
-
if any(counts[pattern] >= repeat_min for pattern in block_patterns(name))
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def pick_groupsize(out_f: int, in_f: int, groupsize) -> int | None:
|
| 182 |
-
"""Какую группу взять этому слою. Число - взять его же или отказать.
|
| 183 |
-
|
| 184 |
-
"auto" спускается по лесенке до первой группы, на которую делится входная
|
| 185 |
-
размерность. Это нужно зрительным башням: у Qwen3-VL ширина 1152 и 4304, ни
|
| 186 |
-
одна из них не кратна 256, поэтому при фиксированной группе всё зрение
|
| 187 |
-
оставалось в bf16. Эталонные сборки supermind делают ровно так же - проверено
|
| 188 |
-
по comfy_quant в qwen3vl_8b_int8_convrot: 256 у языковой части, 64 у
|
| 189 |
-
visual.attn, 16 у visual.mlp.linear_fc2.
|
| 190 |
-
"""
|
| 191 |
-
if groupsize != "auto":
|
| 192 |
-
g = int(groupsize)
|
| 193 |
-
return g if in_f % g == 0 and min(out_f, in_f) >= g else None
|
| 194 |
-
# Мелкая группа - способ достать слой с неудобной шириной, а не разрешение
|
| 195 |
-
# квантовать что угодно узкое. Порог по узкой стороне остаётся базовым (256),
|
| 196 |
-
# иначе в отбор лезут краевые слои: gate_logits (32 x 4096) и входные
|
| 197 |
-
# проекции с пикселей patchify_proj (4096 x 128). Эталонные сборки LTX их не
|
| 198 |
-
# берут - сверено поимённо в tools/audit_reference_int8.py.
|
| 199 |
-
if min(out_f, in_f) < GROUPSIZE_LADDER[0]:
|
| 200 |
-
return None
|
| 201 |
-
for g in GROUPSIZE_LADDER:
|
| 202 |
-
if in_f % g == 0:
|
| 203 |
-
return g
|
| 204 |
-
return None
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
def should_quantize(name: str, t: torch.Tensor, groupsize,
|
| 208 |
-
skip_tokens=None, repeated=None) -> tuple[bool, str, int]:
|
| 209 |
-
if not name.endswith(".weight"):
|
| 210 |
-
return False, "не weight", 0
|
| 211 |
-
if t.ndim != 2:
|
| 212 |
-
return False, f"ndim={t.ndim}", 0
|
| 213 |
-
if repeated is not None and name[: -len(".weight")] not in repeated:
|
| 214 |
-
return False, "одиночный слой (не повторяется по блокам)", 0
|
| 215 |
-
low = name.lower()
|
| 216 |
-
for tok in (SKIP_TOKENS if skip_tokens is None else skip_tokens):
|
| 217 |
-
if tok in low:
|
| 218 |
-
return False, f"имя содержит '{tok}'", 0
|
| 219 |
-
out_f, in_f = t.shape
|
| 220 |
-
g = pick_groupsize(out_f, in_f, groupsize)
|
| 221 |
-
if g is None:
|
| 222 |
-
if in_f % (int(groupsize) if groupsize != "auto" else GROUPSIZE_LADDER[-1]):
|
| 223 |
-
return False, f"in_features {in_f} не кратно {groupsize}", 0
|
| 224 |
-
return False, f"слишком маленький слой {tuple(t.shape)}", 0
|
| 225 |
-
return True, "", g
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
def _cost(residual: torch.Tensor, objective: str, step: torch.Tensor,
|
| 229 |
-
chan_w: torch.Tensor | None = None) -> torch.Tensor:
|
| 230 |
-
"""Во что нам обходится остаток. Сетка значений всегда равномерная - ядро
|
| 231 |
-
разжимает строго как q*scale, кодбука в формате нет. Свободен только критерий,
|
| 232 |
-
по которому выбирается точка подрезания, и от него зависит, насколько сильно
|
| 233 |
-
мы готовы жертвовать выбросами ради основной массы весов.
|
| 234 |
-
|
| 235 |
-
mse - квадрат: выбросы дороги, подрезаем осторожно
|
| 236 |
-
l1 - модуль: выбросы дешевле, подрезаем смелее
|
| 237 |
-
huber - квадрат вблизи нуля, модуль дальше: компромисс
|
| 238 |
-
"""
|
| 239 |
-
# Взвешивание по каналам (AWQ-стиль): ошибка в канале, по которому приходят
|
| 240 |
-
# большие активации, стоит дороже. chan_w - средний квадрат входа по каналам,
|
| 241 |
-
# снятый capture_activations.py уже в повёрнутом пространстве. Это в точности
|
| 242 |
-
# диагональ гессиана слоя, то есть переход от "ошибки весов" к "ошибке выхода".
|
| 243 |
-
if objective == "mse":
|
| 244 |
-
sq = residual ** 2
|
| 245 |
-
if chan_w is not None:
|
| 246 |
-
sq = sq * chan_w
|
| 247 |
-
return sq.sum(dim=1, keepdim=True)
|
| 248 |
-
if objective == "l1":
|
| 249 |
-
a = residual.abs()
|
| 250 |
-
if chan_w is not None:
|
| 251 |
-
a = a * chan_w
|
| 252 |
-
return a.sum(dim=1, keepdim=True)
|
| 253 |
-
if objective == "huber":
|
| 254 |
-
d = step # порог - один шаг кванта
|
| 255 |
-
a = residual.abs()
|
| 256 |
-
quad = torch.minimum(a, d)
|
| 257 |
-
h = 0.5 * quad ** 2 + d * (a - quad)
|
| 258 |
-
if chan_w is not None:
|
| 259 |
-
h = h * chan_w
|
| 260 |
-
return h.sum(dim=1, keepdim=True)
|
| 261 |
-
raise SystemExit(f"неизвестный критерий {objective}")
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
def quantize_search(w: torch.Tensor, groupsize: int, ratios,
|
| 265 |
-
objective: str = "mse",
|
| 266 |
-
qmin: int = -127,
|
| 267 |
-
sign_aware: bool = False,
|
| 268 |
-
ls_refit: bool = False,
|
| 269 |
-
chan_w: torch.Tensor | None = None,
|
| 270 |
-
clip_margin: float = 0.0) -> tuple[torch.Tensor, torch.Tensor]:
|
| 271 |
-
"""INT8 ConvRot с подбором масштаба по минимуму ошибки (вместо простого absmax).
|
| 272 |
-
|
| 273 |
-
Официальные сборки берут scale = absmax/127. Небольшое подрезание выбросов
|
| 274 |
-
почти всегда уменьшает суммарную ошибку: редкие большие веса округляются
|
| 275 |
-
чуть грубее, зато основная масса значений ложится на сетку точнее.
|
| 276 |
-
"""
|
| 277 |
-
from comfy_kitchen.backends.eager.quantization import _build_hadamard, _rotate_weight
|
| 278 |
-
|
| 279 |
-
h = _build_hadamard(groupsize, device=w.device, dtype=w.dtype)
|
| 280 |
-
w_rot = _rotate_weight(w, h, groupsize)
|
| 281 |
-
|
| 282 |
-
absmax = w_rot.abs().amax(dim=1, keepdim=True).clamp(min=1e-12)
|
| 283 |
-
best_scale = absmax / 127.0
|
| 284 |
-
best_err = torch.full_like(absmax, float("inf"))
|
| 285 |
-
|
| 286 |
-
if sign_aware:
|
| 287 |
-
# Единственный честный способ дотянуться до -128: разрешать его только
|
| 288 |
-
# тем строкам, у которых отрицательный хвост и так длиннее положительного.
|
| 289 |
-
# Тогда несимметричная сетка не создаёт перекос, а повторяет уже имеющийся.
|
| 290 |
-
neg_heavy = w_rot.amin(dim=1, keepdim=True).abs() > w_rot.amax(dim=1, keepdim=True)
|
| 291 |
-
lo = torch.where(neg_heavy, -128.0, -127.0).to(w_rot.dtype)
|
| 292 |
-
else:
|
| 293 |
-
lo = torch.full_like(absmax, float(qmin))
|
| 294 |
-
hi = torch.full_like(absmax, 127.0)
|
| 295 |
-
|
| 296 |
-
# qmin=-127 (симметрично) по умолчанию. Значение -128 формально доступно и
|
| 297 |
-
# уменьшает ошибку весов, но вносит односторонний сдвиг: сетка перестаёт быть
|
| 298 |
-
# симметричной, у ошибки появляется ненулевое среднее, и на генерации это
|
| 299 |
-
# выходит дороже выигрыша. Проверено на стенде - см. bench_quant.py.
|
| 300 |
-
for r in ratios:
|
| 301 |
-
scale = absmax * r / 127.0
|
| 302 |
-
q = torch.minimum(torch.maximum(torch.round(w_rot / scale), lo), hi)
|
| 303 |
-
err = _cost(q * scale - w_rot, objective, scale, chan_w)
|
| 304 |
-
better = err < best_err
|
| 305 |
-
best_err = torch.where(better, err, best_err)
|
| 306 |
-
best_scale = torch.where(better, scale, best_scale)
|
| 307 |
-
|
| 308 |
-
if clip_margin > 0.0:
|
| 309 |
-
# Порог на подрезание. Эталонные сборки оставляют без подрезания заметно
|
| 310 |
-
# больше строк, чем даёт чистый минимум MSE (у qwen3-4b 9.4% против наших
|
| 311 |
-
# 2-3%), и на выходе энкодера это оказывается лучше. Похоже, у выбросов
|
| 312 |
-
# есть функциональная роль, которую квадрат ошибки не видит. Поэтому режем
|
| 313 |
-
# только там, где выигрыш действительно заметный.
|
| 314 |
-
s0 = absmax / 127.0
|
| 315 |
-
q0 = torch.minimum(torch.maximum(torch.round(w_rot / s0), lo), hi)
|
| 316 |
-
e0 = _cost(q0 * s0 - w_rot, objective, s0, chan_w)
|
| 317 |
-
keep = best_err >= e0 * (1.0 - clip_margin)
|
| 318 |
-
best_scale = torch.where(keep, s0, best_scale)
|
| 319 |
-
del q0
|
| 320 |
-
|
| 321 |
-
q = torch.minimum(torch.maximum(torch.round(w_rot / best_scale), lo), hi)
|
| 322 |
-
|
| 323 |
-
if ls_refit:
|
| 324 |
-
# При фиксированных целых q оптимальный по МНК масштаб - это (q·w)/(q·q),
|
| 325 |
-
# а не absmax/127. Пересчёт убирает систематический сдвиг реконструкции,
|
| 326 |
-
# который и появляется, когда сетка перестаёт быть симметричной.
|
| 327 |
-
num = (q * w_rot).sum(dim=1, keepdim=True)
|
| 328 |
-
den = (q * q).sum(dim=1, keepdim=True).clamp(min=1.0)
|
| 329 |
-
best_scale = torch.where(num > 0, num / den, best_scale)
|
| 330 |
-
|
| 331 |
-
return q.to(torch.int8), best_scale.to(torch.float32)
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
def quantize_int4(w: torch.Tensor, groupsize: int):
|
| 335 |
-
"""Тот же поворот Адамара, но 4 бита вместо 8.
|
| 336 |
-
|
| 337 |
-
Ядро читает такой слой по маркеру format=convrot_w4a4 и жёстко ожидает
|
| 338 |
-
quant_group_size=64 (см. comfy/ops.py, ветка convrot_w4a4), поэтому размер
|
| 339 |
-
группы квантования не настраиваем.
|
| 340 |
-
"""
|
| 341 |
-
from comfy_kitchen.tensor.convrot_w4a4 import TensorCoreConvRotW4A4Layout
|
| 342 |
-
qdata, params = TensorCoreConvRotW4A4Layout.quantize(
|
| 343 |
-
w, convrot_groupsize=groupsize, quant_group_size=64, linear_dtype="int4")
|
| 344 |
-
return qdata, params.scale
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
def int4_error(w: torch.Tensor, groupsize: int) -> float:
|
| 348 |
-
"""Во сколько раз 4 бита дороже 8 на этом слое. Нужно, чтобы выбрать,
|
| 349 |
-
какие слои не жалко ужать сильнее."""
|
| 350 |
-
from comfy_kitchen.tensor.convrot_w4a4 import TensorCoreConvRotW4A4Layout
|
| 351 |
-
q8, s8 = quantize_search(w, groupsize, SEARCH_RATIOS)
|
| 352 |
-
from comfy_kitchen.tensor.int8 import TensorWiseINT8Layout
|
| 353 |
-
p8 = TensorWiseINT8Layout.Params(scale=s8, orig_dtype=torch.float32,
|
| 354 |
-
orig_shape=tuple(q8.shape), is_weight=True,
|
| 355 |
-
convrot=True, convrot_groupsize=groupsize)
|
| 356 |
-
e8 = (TensorWiseINT8Layout.dequantize(q8.to(w.device), p8) - w).norm().item()
|
| 357 |
-
|
| 358 |
-
q4, params4 = TensorCoreConvRotW4A4Layout.quantize(
|
| 359 |
-
w, convrot_groupsize=groupsize, quant_group_size=64, linear_dtype="int4")
|
| 360 |
-
e4 = (TensorCoreConvRotW4A4Layout.dequantize(q4, params4).float() - w).norm().item()
|
| 361 |
-
return e4 / max(e8, 1e-12)
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
def load_calib(path: str, alpha: float, device: str):
|
| 365 |
-
"""Статистика активаций из capture_activations.py -> веса каналов."""
|
| 366 |
-
if not path:
|
| 367 |
-
return None
|
| 368 |
-
blob = torch.load(path, map_location="cpu")
|
| 369 |
-
stats = blob["stats"]
|
| 370 |
-
out = {}
|
| 371 |
-
for name, v in stats.items():
|
| 372 |
-
w = v.to(device=device, dtype=torch.float32).clamp(min=1e-12)
|
| 373 |
-
w = w / w.mean() # нормируем, чтобы масштаб ошибки не поехал
|
| 374 |
-
out[name] = w.pow(alpha).reshape(1, -1)
|
| 375 |
-
print(f"калибровка: {path}\n слоёв {len(out)}, промптов {blob.get('prompts')}, "
|
| 376 |
-
f"шагов {blob.get('steps')}, показатель {alpha}")
|
| 377 |
-
return out
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
def calib_for(calib, tensor_name: str):
|
| 381 |
-
"""Ключи калибровки - пути модулей, ключи файла - они же плюс '.weight'."""
|
| 382 |
-
if not calib:
|
| 383 |
-
return None
|
| 384 |
-
layer = tensor_name[: -len(".weight")]
|
| 385 |
-
if layer in calib:
|
| 386 |
-
return calib[layer]
|
| 387 |
-
for prefix in ("model.diffusion_model.", "diffusion_model.", "model."):
|
| 388 |
-
if layer.startswith(prefix) and layer[len(prefix):] in calib:
|
| 389 |
-
return calib[layer[len(prefix):]]
|
| 390 |
-
return None
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
def convert(src: str, dst: str, groupsize: int, device: str, dry_run: bool,
|
| 394 |
-
search: bool = False, objective: str = "mse", qmin: int = -127,
|
| 395 |
-
sign_aware: bool = False, ls_refit: bool = False,
|
| 396 |
-
calib_path: str = "", calib_alpha: float = 1.0,
|
| 397 |
-
search_min: float = 0.80, clip_margin: float = 0.0,
|
| 398 |
-
quant_adaln: bool = False, skip_connectors: bool = False,
|
| 399 |
-
select: str = "names", drop_prefixes: tuple = (),
|
| 400 |
-
passthrough_dtype: str = "") -> None:
|
| 401 |
-
t0 = time.time()
|
| 402 |
-
ratios = build_ratios(search_min)
|
| 403 |
-
skip = SKIP_TOKENS
|
| 404 |
-
if skip_connectors:
|
| 405 |
-
# Вернуть старое поведение: не трогать *_embeddings_connector. Это 96 слоёв
|
| 406 |
-
# обычного внимания и полносвязных на 4 ГБ, которые эталонные сборки
|
| 407 |
-
# пропускают. Нужно, чтобы отделить вклад коннекторов от вклада подрезания.
|
| 408 |
-
skip = tuple(t for t in skip if t != "_emb.") + ("_emb",)
|
| 409 |
-
print("режим: коннекторы НЕ квантуются (как в эталонных сборках)")
|
| 410 |
-
if quant_adaln:
|
| 411 |
-
# AdaLN задаёт масштаб и сдвиг каждому блоку, поэтому его везде защищают.
|
| 412 |
-
# Но это восьмибитный шаг, а не четырёхбитный - проверяем замером, а не верой.
|
| 413 |
-
skip = tuple(t for t in SKIP_TOKENS if t not in ("adaln", "modulation"))
|
| 414 |
-
print("режим: AdaLN тоже квантуется")
|
| 415 |
-
if groupsize == "auto":
|
| 416 |
-
print(f"режим: группа подбирается послойно по лесенке {GROUPSIZE_LADDER}")
|
| 417 |
-
out: dict[str, torch.Tensor] = {}
|
| 418 |
-
quantized = skipped = 0
|
| 419 |
-
skip_reasons: dict[str, int] = {}
|
| 420 |
-
gs_used: dict[int, int] = {}
|
| 421 |
-
calib = load_calib(calib_path, calib_alpha, device)
|
| 422 |
-
calib_hits = 0
|
| 423 |
-
|
| 424 |
-
# папка = HF-репозиторий, читаем шарды напрямую; файл = как раньше
|
| 425 |
-
opener = HFShardReader(src) if os.path.isdir(src) else safe_open(src, framework="pt")
|
| 426 |
-
with opener as f:
|
| 427 |
-
keys = list(f.keys())
|
| 428 |
-
# ComfyUI читает конфигурацию модели из метаданных файла (model_detection.py:
|
| 429 |
-
# dit_config.update(json.loads(metadata["config"])...). Без них он строит
|
| 430 |
-
# ��одель по умолчанию, и чекпоинт не грузится: "size mismatch for
|
| 431 |
-
# scale_shift_table: [9, 4096] vs [6, 4096]". Переносим как есть.
|
| 432 |
-
meta = f.metadata()
|
| 433 |
-
print(f"вход: {src}\n тензоров: {len(keys)}"
|
| 434 |
-
f"{', метаданных: ' + str(len(meta)) if meta else ', метаданных нет'}")
|
| 435 |
-
|
| 436 |
-
repeated = None
|
| 437 |
-
if select in ("structure", "both"):
|
| 438 |
-
names = [k[: -len(".weight")] for k in keys if k.endswith(".weight")]
|
| 439 |
-
repeated = repeated_layers(names)
|
| 440 |
-
print(f" повторяющихся по блокам слоёв: {len(repeated)} из {len(names)}")
|
| 441 |
-
if select == "structure":
|
| 442 |
-
skip = () # имена больше не участвуют, отбор чисто структурный
|
| 443 |
-
|
| 444 |
-
for i, k in enumerate(keys, 1):
|
| 445 |
-
if drop_prefixes and k.startswith(drop_prefixes):
|
| 446 |
-
skip_reasons["выброшен(--drop)"] = skip_reasons.get("выброшен(--drop)", 0) + 1
|
| 447 |
-
continue
|
| 448 |
-
t = f.get_tensor(k)
|
| 449 |
-
ok, why, gs = should_quantize(k, t, groupsize, skip, repeated)
|
| 450 |
-
if not ok:
|
| 451 |
-
skipped += 1
|
| 452 |
-
skip_reasons[why.split()[0]] = skip_reasons.get(why.split()[0], 0) + 1
|
| 453 |
-
if passthrough_dtype == "bf16" and t.dtype == torch.float32:
|
| 454 |
-
t = t.to(torch.bfloat16)
|
| 455 |
-
out[k] = t
|
| 456 |
-
continue
|
| 457 |
-
gs_used[gs] = gs_used.get(gs, 0) + 1
|
| 458 |
-
|
| 459 |
-
if dry_run:
|
| 460 |
-
quantized += 1
|
| 461 |
-
continue
|
| 462 |
-
|
| 463 |
-
# поворот и подбор масштаба считаем в float32: в bf16 сама ротация
|
| 464 |
-
# вносит заметную ошибку (проверено на эталонных сборках)
|
| 465 |
-
w = t.to(device=device, dtype=torch.float32)
|
| 466 |
-
# оба режима идут через один и тот же код: absmax - это просто подбор
|
| 467 |
-
# по единственному коэффициенту 1.0
|
| 468 |
-
cw = calib_for(calib, k)
|
| 469 |
-
calib_hits += cw is not None
|
| 470 |
-
qdata, scale = quantize_search(
|
| 471 |
-
w, gs, ratios if search else (1.0,), objective, qmin,
|
| 472 |
-
sign_aware, ls_refit, cw, clip_margin)
|
| 473 |
-
layer = k[: -len(".weight")]
|
| 474 |
-
out[k] = qdata.cpu()
|
| 475 |
-
out[f"{layer}.weight_scale"] = scale.cpu()
|
| 476 |
-
conf = {"format": "int8_tensorwise", "convrot": True, "convrot_groupsize": gs}
|
| 477 |
-
out[f"{layer}.comfy_quant"] = torch.tensor(
|
| 478 |
-
list(json.dumps(conf).encode("utf-8")), dtype=torch.uint8)
|
| 479 |
-
quantized += 1
|
| 480 |
-
del w, qdata, scale
|
| 481 |
-
|
| 482 |
-
if i % 50 == 0 or i == len(keys):
|
| 483 |
-
done = quantized + skipped
|
| 484 |
-
print(f" [{done}/{len(keys)}] квантовано {quantized}, пропущено {skipped}",
|
| 485 |
-
flush=True)
|
| 486 |
-
|
| 487 |
-
print(f"\nитог: квантовано {quantized}, оставлено {skipped}")
|
| 488 |
-
print(" причины пропуска:", skip_reasons)
|
| 489 |
-
print(" размеры групп:", dict(sorted(gs_used.items(), reverse=True)))
|
| 490 |
-
if calib:
|
| 491 |
-
print(f" калибровка нашлась для {calib_hits} из {quantized} слоёв")
|
| 492 |
-
if calib_hits < quantized:
|
| 493 |
-
print(" ВНИМАНИЕ: часть слоёв квантована без калибровки - проверь имена")
|
| 494 |
-
if dry_run:
|
| 495 |
-
print("(dry-run: файл не записан)")
|
| 496 |
-
return
|
| 497 |
-
|
| 498 |
-
os.makedirs(os.path.dirname(os.path.abspath(dst)), exist_ok=True)
|
| 499 |
-
save_file(out, dst, metadata=meta if meta else None)
|
| 500 |
-
src_gb = os.path.getsize(src) / 1e9
|
| 501 |
-
dst_gb = os.path.getsize(dst) / 1e9
|
| 502 |
-
print(f"\nзаписано: {dst}\n {src_gb:.2f} GB -> {dst_gb:.2f} GB за {time.time()-t0:.0f} c")
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
if __name__ == "__main__":
|
| 506 |
-
ap = argparse.ArgumentParser()
|
| 507 |
-
ap.add_argument("src")
|
| 508 |
-
ap.add_argument("dst")
|
| 509 |
-
ap.add_argument("--groupsize", default=256,
|
| 510 |
-
type=lambda v: v if v == "auto" else int(v),
|
| 511 |
-
help="число или auto — тогда группа подбирается послойно "
|
| 512 |
-
f"по лесенке {GROUPSIZE_LADDER}, как в эталонных сборках")
|
| 513 |
-
ap.add_argument("--device", default="cuda:0")
|
| 514 |
-
ap.add_argument("--dry-run", action="store_true")
|
| 515 |
-
ap.add_argument("--no-search-scale", action="store_true",
|
| 516 |
-
help="взять простой absmax вместо подбора масштаба (быстрее, но хуже эталона)")
|
| 517 |
-
ap.add_argument("--objective", default="mse", choices=("mse", "l1", "huber"),
|
| 518 |
-
help="критерий выбора точки подрезания выбросов")
|
| 519 |
-
ap.add_argument("--asymmetric", action="store_true",
|
| 520 |
-
help="р��зрешить -128 всем строкам (ошибка весов меньше, на генерации хуже)")
|
| 521 |
-
ap.add_argument("--sign-aware", action="store_true",
|
| 522 |
-
help="разрешать -128 только строкам с длинным отрицательным хвостом")
|
| 523 |
-
ap.add_argument("--ls-refit", action="store_true",
|
| 524 |
-
help="пересчитать масштаб по МНК после выбора целых значений")
|
| 525 |
-
ap.add_argument("--calib", default="",
|
| 526 |
-
help="файл статистики активаций от capture_activations.py")
|
| 527 |
-
ap.add_argument("--calib-alpha", type=float, default=1.0,
|
| 528 |
-
help="показатель степени для веса канала: 1.0 - диагональ гессиана, "
|
| 529 |
-
"0.5 - мягче, 0.0 - как без калибровки")
|
| 530 |
-
ap.add_argument("--search-min", type=float, default=0.80,
|
| 531 |
-
help="нижняя граница перебора коэффициентов подрезания")
|
| 532 |
-
ap.add_argument("--skip-connectors", action="store_true",
|
| 533 |
-
help="не квантовать *_embeddings_connector, как в эталонных сборках")
|
| 534 |
-
ap.add_argument("--quant-adaln", action="store_true",
|
| 535 |
-
help="квантовать и слои модуляции AdaLN (ещё ~0.34 ГБ экономии)")
|
| 536 |
-
ap.add_argument("--clip-margin", type=float, default=0.0,
|
| 537 |
-
help="подрезать строку, только если ошибка падает больше чем на эту долю "
|
| 538 |
-
"(0.03 = на 3%%); иначе оставить absmax")
|
| 539 |
-
ap.add_argument("--select", default="names", choices=("names", "structure", "both"),
|
| 540 |
-
help="как отбирать слои: names — по списку подстрок (как раньше); "
|
| 541 |
-
"structure — по повторяемости в стопке блоков, без имён; "
|
| 542 |
-
"both — оба условия сразу")
|
| 543 |
-
ap.add_argument("--drop-prefixes", default="",
|
| 544 |
-
help="префиксы через запятую: такие тензоры НЕ переносить в выход "
|
| 545 |
-
"вовсе (для LTX: vae.,audio_vae.,vocoder.,text_embedding_projection. "
|
| 546 |
-
"— компоненты лежат отдельными файлами, дубли не нужны)")
|
| 547 |
-
ap.add_argument("--passthrough-dtype", default="", choices=("", "bf16"),
|
| 548 |
-
help="во что кастовать НЕквантуемые тензоры (для fp32-исходников "
|
| 549 |
-
"вроде HiDream-O1: без этого эмбеддинги уедут в выход в fp32)")
|
| 550 |
-
a = ap.parse_args()
|
| 551 |
-
convert(a.src, a.dst, a.groupsize, a.device, a.dry_run,
|
| 552 |
-
not a.no_search_scale, a.objective, -128 if a.asymmetric else -127,
|
| 553 |
-
a.sign_aware, a.ls_refit, a.calib, a.calib_alpha, a.search_min,
|
| 554 |
-
a.clip_margin, a.quant_adaln, a.skip_connectors, a.select,
|
| 555 |
-
tuple(p for p in a.drop_prefixes.split(",") if p),
|
| 556 |
-
a.passthrough_dtype)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/mm_profile_main_quant.py
DELETED
|
@@ -1,143 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""Profile sampled INT8 convrot reconstruction error for all MiniMax-H3 DiT weights.
|
| 3 |
-
|
| 4 |
-
The profiler keeps every input column (so Hadamard rotation is exact) but samples
|
| 5 |
-
output rows deterministically. It is intended for choosing a small BF16 island;
|
| 6 |
-
the final converter still quantizes every retained INT8 tensor in full.
|
| 7 |
-
"""
|
| 8 |
-
import argparse
|
| 9 |
-
import json
|
| 10 |
-
import os
|
| 11 |
-
import sys
|
| 12 |
-
import time
|
| 13 |
-
|
| 14 |
-
import torch
|
| 15 |
-
|
| 16 |
-
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 17 |
-
ROOT = os.environ.get("MINIMAX_H3_ROOT", os.getcwd())
|
| 18 |
-
TOOLS = SCRIPT_DIR
|
| 19 |
-
sys.path.insert(0, TOOLS)
|
| 20 |
-
|
| 21 |
-
from convert_int8_convrot import build_ratios, quantize_search # noqa: E402
|
| 22 |
-
from mm_quantize_lean import ShardReader # noqa: E402
|
| 23 |
-
from comfy_kitchen.backends.eager.quantization import _build_hadamard, _rotate_weight # noqa: E402
|
| 24 |
-
|
| 25 |
-
sys.stdout.reconfigure(encoding="utf-8")
|
| 26 |
-
torch.set_grad_enabled(False)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
SHAPES = {
|
| 30 |
-
"attn.qkv_proj": (21504, 5376),
|
| 31 |
-
"attn.out_proj": (5376, 7168),
|
| 32 |
-
"mlp.fc1": (28672, 5376),
|
| 33 |
-
"mlp.fc2": (5376, 14336),
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def source_weight(reader, block, kind):
|
| 38 |
-
prefix = f"transformer_blocks.{block}"
|
| 39 |
-
if kind == "attn.qkv_proj":
|
| 40 |
-
return torch.cat([reader.get_tensor(f"{prefix}.attn.to_{part}.weight") for part in "qkv"], dim=0)
|
| 41 |
-
if kind == "attn.out_proj":
|
| 42 |
-
return reader.get_tensor(f"{prefix}.attn.to_out.0.weight")
|
| 43 |
-
if kind == "mlp.fc1":
|
| 44 |
-
weight = reader.get_tensor(f"{prefix}.ff.net.0.proj.weight")
|
| 45 |
-
half = weight.shape[0] // 2
|
| 46 |
-
return torch.cat((weight[half:], weight[:half]), dim=0)
|
| 47 |
-
if kind == "mlp.fc2":
|
| 48 |
-
return reader.get_tensor(f"{prefix}.ff.net.2.weight")
|
| 49 |
-
raise ValueError(kind)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def sampled_rows(weight, count):
|
| 53 |
-
if weight.shape[0] <= count:
|
| 54 |
-
return weight
|
| 55 |
-
index = torch.linspace(0, weight.shape[0] - 1, count, dtype=torch.float64).round().long()
|
| 56 |
-
return weight.index_select(0, index)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def profile(weight, groupsize, rows, ratios, device, clip_margin):
|
| 60 |
-
sample = sampled_rows(weight, rows).to(device=device, dtype=torch.float32)
|
| 61 |
-
hadamard = _build_hadamard(groupsize, device=sample.device, dtype=sample.dtype)
|
| 62 |
-
rotated = _rotate_weight(sample, hadamard, groupsize)
|
| 63 |
-
absmax = rotated.abs().amax(dim=1, keepdim=True).clamp(min=1e-12)
|
| 64 |
-
scale0 = absmax / 127.0
|
| 65 |
-
q0 = torch.round(rotated / scale0).clamp(-127, 127)
|
| 66 |
-
err0 = ((q0 * scale0 - rotated).norm() / rotated.norm()).item()
|
| 67 |
-
q, scale = quantize_search(sample, groupsize, ratios, clip_margin=clip_margin)
|
| 68 |
-
err = ((q.float() * scale - rotated).norm() / rotated.norm()).item()
|
| 69 |
-
clipped = (scale < scale0 * 0.9999).float().mean().item()
|
| 70 |
-
del sample, hadamard, rotated, absmax, scale0, q0, q, scale
|
| 71 |
-
return err0, err, clipped
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def main():
|
| 75 |
-
ap = argparse.ArgumentParser()
|
| 76 |
-
ap.add_argument("variant", choices=("fl2va", "ref2va"))
|
| 77 |
-
ap.add_argument("--src", default="", help="raw diffusers transformer directory")
|
| 78 |
-
ap.add_argument("--device", default="cuda:1")
|
| 79 |
-
ap.add_argument("--rows", type=int, default=192)
|
| 80 |
-
ap.add_argument("--blocks", type=int, default=50,
|
| 81 |
-
help="число первых блоков; только для короткой проверки профайлера")
|
| 82 |
-
ap.add_argument("--groupsize", type=int, choices=(16, 64, 256), default=256)
|
| 83 |
-
ap.add_argument("--search-min", type=float, default=0.80)
|
| 84 |
-
ap.add_argument("--clip-margin", type=float, default=0.05)
|
| 85 |
-
ap.add_argument("--out", default="")
|
| 86 |
-
args = ap.parse_args()
|
| 87 |
-
|
| 88 |
-
source = args.src or os.path.join(ROOT, "HF",
|
| 89 |
-
"transformer" if args.variant == "fl2va" else "transformer_ref")
|
| 90 |
-
out = args.out or os.path.join(ROOT, "comfy_headless", f"mm_quant_profile_{args.variant}.json")
|
| 91 |
-
ratios = build_ratios(args.search_min)
|
| 92 |
-
results = []
|
| 93 |
-
started = time.time()
|
| 94 |
-
print(f"source={source}\ndevice={args.device} rows={args.rows} g={args.groupsize}\nout={out}", flush=True)
|
| 95 |
-
|
| 96 |
-
with ShardReader(source) as reader:
|
| 97 |
-
for block in range(args.blocks):
|
| 98 |
-
block_started = time.time()
|
| 99 |
-
for kind, shape in SHAPES.items():
|
| 100 |
-
weight = source_weight(reader, block, kind)
|
| 101 |
-
if tuple(weight.shape) != shape:
|
| 102 |
-
raise RuntimeError(f"blocks.{block}.{kind}: {tuple(weight.shape)} != {shape}")
|
| 103 |
-
err0, err, clipped = profile(weight, args.groupsize, args.rows, ratios,
|
| 104 |
-
args.device, args.clip_margin)
|
| 105 |
-
count = weight.numel()
|
| 106 |
-
out_features = weight.shape[0]
|
| 107 |
-
int8_bytes = count + out_features * 4 + len(json.dumps({
|
| 108 |
-
"format": "int8_tensorwise", "convrot": True,
|
| 109 |
-
"convrot_groupsize": args.groupsize}).encode("utf-8"))
|
| 110 |
-
bf16_delta = count * 2 - int8_bytes
|
| 111 |
-
results.append({
|
| 112 |
-
"layer": f"blocks.{block}.{kind}",
|
| 113 |
-
"block": block,
|
| 114 |
-
"kind": kind,
|
| 115 |
-
"shape": list(weight.shape),
|
| 116 |
-
"sample_rows": min(args.rows, weight.shape[0]),
|
| 117 |
-
"absmax_error": err0,
|
| 118 |
-
"searched_error": err,
|
| 119 |
-
"improvement": err0 - err,
|
| 120 |
-
"clipped_row_fraction": clipped,
|
| 121 |
-
"bf16_delta_bytes": bf16_delta,
|
| 122 |
-
})
|
| 123 |
-
del weight
|
| 124 |
-
print(f"block {block:02d}/49: {time.time() - block_started:.1f}s, total {time.time() - started:.0f}s",
|
| 125 |
-
flush=True)
|
| 126 |
-
|
| 127 |
-
payload = {
|
| 128 |
-
"variant": args.variant,
|
| 129 |
-
"source": source,
|
| 130 |
-
"groupsize": args.groupsize,
|
| 131 |
-
"rows": args.rows,
|
| 132 |
-
"search_min": args.search_min,
|
| 133 |
-
"clip_margin": args.clip_margin,
|
| 134 |
-
"elapsed_seconds": time.time() - started,
|
| 135 |
-
"layers": results,
|
| 136 |
-
}
|
| 137 |
-
with open(out, "w", encoding="utf-8") as stream:
|
| 138 |
-
json.dump(payload, stream, ensure_ascii=False, indent=2)
|
| 139 |
-
print(f"готово: {out}; слоёв={len(results)}, {time.time() - started:.0f}s", flush=True)
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
if __name__ == "__main__":
|
| 143 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/mm_quantize_lean.py
DELETED
|
@@ -1,562 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""Build MiniMax-H3 INT8 lean-convrot + high-precision adaLN curve.
|
| 3 |
-
|
| 4 |
-
The input is the original diffusers transformer/transformer_ref directory.
|
| 5 |
-
Q/K/V packing and fc1 ordering are converted while streaming, so the script
|
| 6 |
-
does not need a 61.7 GiB merged BF16 file or a 20-44 GiB tensor dictionary in
|
| 7 |
-
RAM.
|
| 8 |
-
"""
|
| 9 |
-
import argparse
|
| 10 |
-
import json
|
| 11 |
-
import math
|
| 12 |
-
import os
|
| 13 |
-
import re
|
| 14 |
-
import struct
|
| 15 |
-
import sys
|
| 16 |
-
import time
|
| 17 |
-
|
| 18 |
-
import torch
|
| 19 |
-
import torch.nn.functional as F
|
| 20 |
-
from safetensors import safe_open
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 24 |
-
ROOT = os.environ.get("MINIMAX_H3_ROOT", os.getcwd())
|
| 25 |
-
HF = os.path.join(ROOT, "HF")
|
| 26 |
-
COMFYORG = os.path.join(ROOT, "comfyorg")
|
| 27 |
-
TOOLS = SCRIPT_DIR
|
| 28 |
-
MODELS = os.path.join(ROOT, "output")
|
| 29 |
-
|
| 30 |
-
sys.path.insert(0, TOOLS)
|
| 31 |
-
from convert_int8_convrot import build_ratios, quantize_search # noqa: E402
|
| 32 |
-
|
| 33 |
-
sys.stdout.reconfigure(encoding="utf-8")
|
| 34 |
-
torch.set_grad_enabled(False)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
DTYPE_BYTES = {"F64": 8, "F32": 4, "F16": 2, "BF16": 2, "I64": 8,
|
| 38 |
-
"I32": 4, "I16": 2, "I8": 1, "U8": 1, "BOOL": 1}
|
| 39 |
-
TORCH_TO_ST = {torch.float64: "F64", torch.float32: "F32", torch.float16: "F16",
|
| 40 |
-
torch.bfloat16: "BF16", torch.int64: "I64", torch.int32: "I32",
|
| 41 |
-
torch.int16: "I16", torch.int8: "I8", torch.uint8: "U8",
|
| 42 |
-
torch.bool: "BOOL"}
|
| 43 |
-
def quant_blob(groupsize):
|
| 44 |
-
return json.dumps({"format": "int8_tensorwise", "convrot": True,
|
| 45 |
-
"convrot_groupsize": groupsize}).encode("utf-8")
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def block_layers(blocks, suffix):
|
| 49 |
-
return tuple(f"blocks.{block}.{suffix}" for block in blocks)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
# Deterministic 64-row/full-column FL2VA audit. Attention output projections
|
| 53 |
-
# are both the most error-prone family and the cheapest useful BF16 upgrade.
|
| 54 |
-
OUT_PROJ_ERR_GE_105 = (2, 1, 0, 3, 5, 7, 15, 38, 49, 6, 9, 45, 4, 19)
|
| 55 |
-
OUT_PROJ_ERR_GE_100 = OUT_PROJ_ERR_GE_105 + (14, 43, 8, 17, 10, 11, 47, 27, 44, 12, 13, 46, 20)
|
| 56 |
-
TOP_FC2 = (49, 45, 39, 29, 44)
|
| 57 |
-
|
| 58 |
-
QUALITY_PROFILES = {
|
| 59 |
-
"compact": (),
|
| 60 |
-
# ~20.39 GiB with dynamic rank16.
|
| 61 |
-
"quality20": block_layers(OUT_PROJ_ERR_GE_105, "attn.out_proj")
|
| 62 |
-
+ block_layers(TOP_FC2[:1], "mlp.fc2"),
|
| 63 |
-
# ~21.00 GiB: recommended first 24 GiB load/generation candidate.
|
| 64 |
-
"quality21": block_layers(OUT_PROJ_ERR_GE_100, "attn.out_proj")
|
| 65 |
-
+ block_layers(TOP_FC2[:3], "mlp.fc2"),
|
| 66 |
-
# ~21.97 GiB: only after quality21 demonstrates enough activation headroom.
|
| 67 |
-
"quality22": block_layers(range(50), "attn.out_proj")
|
| 68 |
-
+ block_layers(TOP_FC2, "mlp.fc2"),
|
| 69 |
-
}
|
| 70 |
-
def tensor_bytes(dtype, shape):
|
| 71 |
-
total = DTYPE_BYTES[dtype]
|
| 72 |
-
for dim in shape:
|
| 73 |
-
total *= dim
|
| 74 |
-
return total
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
class ShardReader:
|
| 78 |
-
def __init__(self, path):
|
| 79 |
-
self.path = os.path.abspath(path)
|
| 80 |
-
indexes = [name for name in os.listdir(self.path) if name.endswith(".safetensors.index.json")]
|
| 81 |
-
if len(indexes) != 1:
|
| 82 |
-
raise SystemExit(f"ожидался один *.safetensors.index.json в {self.path}, найдено {len(indexes)}")
|
| 83 |
-
index = json.load(open(os.path.join(self.path, indexes[0]), encoding="utf-8"))
|
| 84 |
-
self.weight_map = index["weight_map"]
|
| 85 |
-
self._files = {}
|
| 86 |
-
for name in sorted(set(self.weight_map.values())):
|
| 87 |
-
full = os.path.join(self.path, name)
|
| 88 |
-
if not os.path.isfile(full):
|
| 89 |
-
raise SystemExit(f"нет шарда из index: {full}")
|
| 90 |
-
self._files[name] = safe_open(full, framework="pt")
|
| 91 |
-
|
| 92 |
-
def keys(self):
|
| 93 |
-
return list(self.weight_map)
|
| 94 |
-
|
| 95 |
-
def get_slice(self, key):
|
| 96 |
-
return self._files[self.weight_map[key]].get_slice(key)
|
| 97 |
-
|
| 98 |
-
def get_tensor(self, key):
|
| 99 |
-
return self._files[self.weight_map[key]].get_tensor(key)
|
| 100 |
-
|
| 101 |
-
def close(self):
|
| 102 |
-
self._files.clear()
|
| 103 |
-
|
| 104 |
-
def __enter__(self):
|
| 105 |
-
return self
|
| 106 |
-
|
| 107 |
-
def __exit__(self, exc_type, exc, traceback):
|
| 108 |
-
self.close()
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def validate_source(reader):
|
| 112 |
-
keys = set(reader.keys())
|
| 113 |
-
if len(keys) != 638:
|
| 114 |
-
raise SystemExit(f"неожиданное число исходных тензоров: {len(keys)}, ожидалось 638")
|
| 115 |
-
shapes = {
|
| 116 |
-
"transformer_blocks.0.attn.to_q.weight": [7168, 5376],
|
| 117 |
-
"transformer_blocks.0.attn.to_k.weight": [7168, 5376],
|
| 118 |
-
"transformer_blocks.0.attn.to_v.weight": [7168, 5376],
|
| 119 |
-
"transformer_blocks.0.attn.to_out.0.weight": [5376, 7168],
|
| 120 |
-
"transformer_blocks.0.ff.net.0.proj.weight": [28672, 5376],
|
| 121 |
-
"transformer_blocks.0.ff.net.2.weight": [5376, 14336],
|
| 122 |
-
"transformer_blocks.0.adaln_proj.linear.weight": [96768, 2688],
|
| 123 |
-
"norm_out.linear.weight": [10752, 2688],
|
| 124 |
-
"time_embedder.linear_1.weight": [5376, 256],
|
| 125 |
-
"time_embedder.linear_2.weight": [2688, 5376],
|
| 126 |
-
}
|
| 127 |
-
for key, expected_shape in shapes.items():
|
| 128 |
-
if key not in keys:
|
| 129 |
-
raise SystemExit(f"в источнике нет {key}")
|
| 130 |
-
actual = reader.get_slice(key).get_shape()
|
| 131 |
-
if actual != expected_shape:
|
| 132 |
-
raise SystemExit(f"форма {key}: {actual}, ожидалось {expected_shape}")
|
| 133 |
-
print("источник diffusers: 638 тензоров, 50 DiT-блоков, формы верны")
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def task(kind, entries, source=None, prefix=None, **extra):
|
| 137 |
-
out = {"kind": kind, "entries": entries, "source": source, "prefix": prefix}
|
| 138 |
-
out.update(extra)
|
| 139 |
-
return out
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def build_tasks(reader, rank, grid, time_mode="dynamic", bf16_layers=(), groupsize=256):
|
| 143 |
-
tasks = []
|
| 144 |
-
used = set()
|
| 145 |
-
quant_count = curve_count = 0
|
| 146 |
-
bf16_layers = set(bf16_layers)
|
| 147 |
-
seen_main_layers = set()
|
| 148 |
-
|
| 149 |
-
def add_copy(out_key, src_key, kind="copy"):
|
| 150 |
-
sl = reader.get_slice(src_key)
|
| 151 |
-
tasks.append(task(kind, [(out_key, sl.get_dtype(), sl.get_shape())], source=src_key))
|
| 152 |
-
used.add(src_key)
|
| 153 |
-
|
| 154 |
-
def add_qkv(out_key, src_prefix, quantized, groupsize=256):
|
| 155 |
-
sources = tuple(src_prefix + f".attn.to_{name}.weight" for name in ("q", "k", "v"))
|
| 156 |
-
shape = reader.get_slice(sources[0]).get_shape()
|
| 157 |
-
out_shape = [shape[0] * 3, shape[1]]
|
| 158 |
-
if quantized:
|
| 159 |
-
layer = out_key[:-len(".weight")]
|
| 160 |
-
blob = quant_blob(groupsize)
|
| 161 |
-
entries = [(out_key, "I8", out_shape),
|
| 162 |
-
(layer + ".weight_scale", "F32", [out_shape[0], 1]),
|
| 163 |
-
(layer + ".comfy_quant", "U8", [len(blob)])]
|
| 164 |
-
tasks.append(task("quant_qkv", entries, source=sources, groupsize=groupsize, blob=blob))
|
| 165 |
-
else:
|
| 166 |
-
tasks.append(task("copy_qkv", [(out_key, reader.get_slice(sources[0]).get_dtype(), out_shape)], source=sources))
|
| 167 |
-
used.update(sources)
|
| 168 |
-
|
| 169 |
-
def add_quant(out_key, src_key, kind="quant", groupsize=256):
|
| 170 |
-
nonlocal quant_count
|
| 171 |
-
shape = reader.get_slice(src_key).get_shape()
|
| 172 |
-
layer = out_key[:-len(".weight")]
|
| 173 |
-
blob = quant_blob(groupsize)
|
| 174 |
-
entries = [(out_key, "I8", shape),
|
| 175 |
-
(layer + ".weight_scale", "F32", [shape[0], 1]),
|
| 176 |
-
(layer + ".comfy_quant", "U8", [len(blob)])]
|
| 177 |
-
tasks.append(task(kind, entries, source=src_key, groupsize=groupsize, blob=blob))
|
| 178 |
-
used.add(src_key)
|
| 179 |
-
quant_count += 1
|
| 180 |
-
|
| 181 |
-
def add_main_qkv(out_key, src_prefix):
|
| 182 |
-
nonlocal quant_count
|
| 183 |
-
layer = out_key[:-len(".weight")]
|
| 184 |
-
seen_main_layers.add(layer)
|
| 185 |
-
quantized = layer not in bf16_layers
|
| 186 |
-
add_qkv(out_key, src_prefix, quantized=quantized, groupsize=groupsize)
|
| 187 |
-
if quantized:
|
| 188 |
-
quant_count += 1
|
| 189 |
-
|
| 190 |
-
def add_main(out_key, src_key, quant_kind="quant", copy_kind="copy"):
|
| 191 |
-
layer = out_key[:-len(".weight")]
|
| 192 |
-
seen_main_layers.add(layer)
|
| 193 |
-
if layer in bf16_layers:
|
| 194 |
-
add_copy(out_key, src_key, kind=copy_kind)
|
| 195 |
-
else:
|
| 196 |
-
add_quant(out_key, src_key, kind=quant_kind, groupsize=groupsize)
|
| 197 |
-
|
| 198 |
-
def add_curve(out_prefix, src_prefix):
|
| 199 |
-
nonlocal curve_count
|
| 200 |
-
weight_key, bias_key = src_prefix + ".weight", src_prefix + ".bias"
|
| 201 |
-
out_features = reader.get_slice(weight_key).get_shape()[0]
|
| 202 |
-
tasks.append(task("curve", [(out_prefix + ".weight", "F32", [out_features, rank]),
|
| 203 |
-
(out_prefix + ".bias", "F32", [out_features])],
|
| 204 |
-
source=src_prefix, prefix=out_prefix))
|
| 205 |
-
used.update((weight_key, bias_key))
|
| 206 |
-
curve_count += 1
|
| 207 |
-
|
| 208 |
-
for block in range(50):
|
| 209 |
-
src = f"transformer_blocks.{block}"
|
| 210 |
-
out = f"blocks.{block}"
|
| 211 |
-
add_main_qkv(out + ".attn.qkv_proj.weight", src)
|
| 212 |
-
add_main(out + ".attn.out_proj.weight", src + ".attn.to_out.0.weight")
|
| 213 |
-
add_copy(out + ".attn.q_norm.weight", src + ".attn.norm_q.weight")
|
| 214 |
-
add_copy(out + ".attn.k_norm.weight", src + ".attn.norm_k.weight")
|
| 215 |
-
add_main(out + ".mlp.fc1.weight", src + ".ff.net.0.proj.weight",
|
| 216 |
-
quant_kind="quant_fc1", copy_kind="copy_fc1")
|
| 217 |
-
add_main(out + ".mlp.fc2.weight", src + ".ff.net.2.weight")
|
| 218 |
-
add_copy(out + ".norm1.weight", src + ".norm1.weight")
|
| 219 |
-
add_copy(out + ".norm2.weight", src + ".norm2.weight")
|
| 220 |
-
add_curve(out + ".adaln_proj.linear", src + ".adaln_proj.linear")
|
| 221 |
-
|
| 222 |
-
for block in range(2):
|
| 223 |
-
src = f"token_refiner.refiner_blocks.{block}"
|
| 224 |
-
out = f"token_refiner.blocks.{block}"
|
| 225 |
-
add_qkv(out + ".attn.qkv_proj.weight", src, quantized=False)
|
| 226 |
-
add_copy(out + ".attn.out_proj.weight", src + ".attn.to_out.0.weight")
|
| 227 |
-
add_copy(out + ".attn.q_norm.weight", src + ".attn.norm_q.weight")
|
| 228 |
-
add_copy(out + ".attn.k_norm.weight", src + ".attn.norm_k.weight")
|
| 229 |
-
add_copy(out + ".mlp.fc1.weight", src + ".ff.net.0.proj.weight", kind="copy_fc1")
|
| 230 |
-
add_copy(out + ".mlp.fc2.weight", src + ".ff.net.2.weight")
|
| 231 |
-
add_copy(out + ".norm1.weight", src + ".norm1.weight")
|
| 232 |
-
add_copy(out + ".norm2.weight", src + ".norm2.weight")
|
| 233 |
-
|
| 234 |
-
add_copy("token_refiner.final_norm.weight", "token_refiner.final_norm.weight")
|
| 235 |
-
for out_key, src_key in (
|
| 236 |
-
("condition_proj.weight", "context_embedder.weight"),
|
| 237 |
-
("condition_proj.bias", "context_embedder.bias"),
|
| 238 |
-
("video_patch_proj.weight", "proj_in.weight"),
|
| 239 |
-
("video_patch_proj.bias", "proj_in.bias"),
|
| 240 |
-
("audio_patch_proj.weight", "audio_proj_in.weight"),
|
| 241 |
-
("audio_patch_proj.bias", "audio_proj_in.bias"),
|
| 242 |
-
("final_layer.video_out.weight", "proj_out.weight"),
|
| 243 |
-
("final_layer.video_out.bias", "proj_out.bias"),
|
| 244 |
-
("final_layer.audio_out.weight", "audio_proj_out.weight"),
|
| 245 |
-
("final_layer.audio_out.bias", "audio_proj_out.bias"),
|
| 246 |
-
("final_layer.norm.weight", "norm_out.norm.weight"),
|
| 247 |
-
):
|
| 248 |
-
add_copy(out_key, src_key)
|
| 249 |
-
add_curve("final_layer.adaln_proj.linear", "norm_out.linear")
|
| 250 |
-
|
| 251 |
-
time_pairs = (
|
| 252 |
-
("time_embedder.proj_in.weight", "time_embedder.linear_1.weight"),
|
| 253 |
-
("time_embedder.proj_in.bias", "time_embedder.linear_1.bias"),
|
| 254 |
-
("time_embedder.proj_out.weight", "time_embedder.linear_2.weight"),
|
| 255 |
-
("time_embedder.proj_out.bias", "time_embedder.linear_2.bias"),
|
| 256 |
-
)
|
| 257 |
-
if time_mode == "dynamic":
|
| 258 |
-
for out_key, src_key in time_pairs:
|
| 259 |
-
add_copy(out_key, src_key)
|
| 260 |
-
tasks.append(task("basis", [("adaln_curve_basis", "F32", [2688, rank]),
|
| 261 |
-
("adaln_curve_mean", "F32", [2688])]))
|
| 262 |
-
elif time_mode == "table":
|
| 263 |
-
used.update(src for _, src in time_pairs)
|
| 264 |
-
tasks.append(task("table", [("adaln_t_table", "F32", [grid, rank])]))
|
| 265 |
-
else:
|
| 266 |
-
raise ValueError(time_mode)
|
| 267 |
-
missing, extra = set(reader.keys()) - used, used - set(reader.keys())
|
| 268 |
-
if missing or extra:
|
| 269 |
-
raise SystemExit(f"маппинг diffusers неполон: не использовано {len(missing)}, неизвестных {len(extra)}; {sorted(missing)[:3]}")
|
| 270 |
-
tasks.append(task("rope", [("rope.inv_freq", "F32", [16])]))
|
| 271 |
-
unknown_bf16 = bf16_layers - seen_main_layers
|
| 272 |
-
if unknown_bf16:
|
| 273 |
-
raise SystemExit(f"неизвестные BF16-слои: {sorted(unknown_bf16)}")
|
| 274 |
-
expected_quant = 200 - len(bf16_layers)
|
| 275 |
-
if quant_count != expected_quant or curve_count != 51:
|
| 276 |
-
raise SystemExit(f"план неполон: INT8={quant_count}/{expected_quant}, adaLN={curve_count}/51")
|
| 277 |
-
entries = [entry for item in tasks for entry in item["entries"]]
|
| 278 |
-
expected_entries = (937 if time_mode == "dynamic" else 932) - 2 * len(bf16_layers)
|
| 279 |
-
if len(entries) != expected_entries or len({entry[0] for entry in entries}) != len(entries):
|
| 280 |
-
raise SystemExit(f"неверный выходной keyset: {len(entries)} вместо {expected_entries}")
|
| 281 |
-
return tasks
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
def inspect_reference(tasks, path, time_mode="dynamic", bf16_layers=()):
|
| 285 |
-
planned = {key: (dtype, shape) for item in tasks for key, dtype, shape in item["entries"]}
|
| 286 |
-
bf16_layers = set(bf16_layers)
|
| 287 |
-
with safe_open(path, framework="pt") as ref:
|
| 288 |
-
ref_keys = set(ref.keys())
|
| 289 |
-
expected_missing = set()
|
| 290 |
-
for layer in bf16_layers:
|
| 291 |
-
expected_missing.update((layer + ".weight_scale", layer + ".comfy_quant"))
|
| 292 |
-
expected_extra = set()
|
| 293 |
-
if time_mode == "dynamic":
|
| 294 |
-
expected_missing.add("adaln_t_table")
|
| 295 |
-
expected_extra.update({"adaln_curve_basis", "adaln_curve_mean",
|
| 296 |
-
"time_embedder.proj_in.weight", "time_embedder.proj_in.bias",
|
| 297 |
-
"time_embedder.proj_out.weight", "time_embedder.proj_out.bias"})
|
| 298 |
-
actual_missing = ref_keys - set(planned)
|
| 299 |
-
actual_extra = set(planned) - ref_keys
|
| 300 |
-
if actual_missing != expected_missing or actual_extra != expected_extra:
|
| 301 |
-
raise SystemExit("keyset расходится с ожидаемым расширением pruned-формата: "
|
| 302 |
-
f"missing={sorted(actual_missing ^ expected_missing)[:5]} "
|
| 303 |
-
f"extra={sorted(actual_extra ^ expected_extra)[:5]}")
|
| 304 |
-
mismatches = []
|
| 305 |
-
for key, (dtype, shape) in planned.items():
|
| 306 |
-
if key not in ref_keys or "adaln_proj.linear" in key or key == "adaln_t_table":
|
| 307 |
-
continue
|
| 308 |
-
sl = ref.get_slice(key)
|
| 309 |
-
layer = key[:-len(".weight")] if key.endswith(".weight") else ""
|
| 310 |
-
dtype_mismatch = sl.get_dtype() != dtype and layer not in bf16_layers
|
| 311 |
-
if dtype_mismatch or sl.get_shape() != shape:
|
| 312 |
-
mismatches.append((key, dtype, shape, sl.get_dtype(), sl.get_shape()))
|
| 313 |
-
if mismatches:
|
| 314 |
-
raise SystemExit(f"формат расходится с pruned-эталоном вне curve-части: {mismatches[:3]}")
|
| 315 |
-
print(f"эталон comfyorg-pruned: общие формы совпадают; dynamic={time_mode == 'dynamic'}, "
|
| 316 |
-
f"BF16 main={len(bf16_layers)}")
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
def make_curve(reader, rank, grid, device):
|
| 320 |
-
w1 = reader.get_tensor("time_embedder.linear_1.weight").to(device=device, dtype=torch.float32)
|
| 321 |
-
b1 = reader.get_tensor("time_embedder.linear_1.bias").to(device=device, dtype=torch.float32)
|
| 322 |
-
w2 = reader.get_tensor("time_embedder.linear_2.weight").to(device=device, dtype=torch.float32)
|
| 323 |
-
b2 = reader.get_tensor("time_embedder.linear_2.bias").to(device=device, dtype=torch.float32)
|
| 324 |
-
half = 128
|
| 325 |
-
freqs = torch.exp(-math.log(10000.0) * torch.arange(half, dtype=torch.float32, device=device) / half)
|
| 326 |
-
|
| 327 |
-
def evaluate(t):
|
| 328 |
-
args = t[:, None] * freqs[None]
|
| 329 |
-
emb = torch.cat((torch.cos(args), torch.sin(args)), dim=1)
|
| 330 |
-
return F.silu(F.linear(F.silu(F.linear(emb, w1, b1)), w2, b2))
|
| 331 |
-
|
| 332 |
-
# 2049 points are sufficient to determine the shared subspace. The output
|
| 333 |
-
# table may be denser: Comfy derives the grid length from table.shape[0].
|
| 334 |
-
fit_t = torch.linspace(0.0, 1.0, 2049, dtype=torch.float32, device=device)
|
| 335 |
-
curve = evaluate(fit_t)
|
| 336 |
-
# Evaluate the real runtime formula in F32, then solve the tiny singular
|
| 337 |
-
# directions in F64 on CPU. F32 SVD bottoms out at ~1.09e-6 relative error
|
| 338 |
-
# for rank 16; the F64 solve followed by F32 storage reaches ~2.72e-7.
|
| 339 |
-
curve_cpu = curve.cpu()
|
| 340 |
-
curve64 = curve_cpu.double()
|
| 341 |
-
mean64 = curve64.mean(0)
|
| 342 |
-
centered64 = curve64 - mean64
|
| 343 |
-
_, singular, vh = torch.linalg.svd(centered64, full_matrices=False)
|
| 344 |
-
basis64 = vh[:rank].T.contiguous()
|
| 345 |
-
basis = basis64.to(device=device, dtype=torch.float32)
|
| 346 |
-
mean = mean64.to(device=device, dtype=torch.float32)
|
| 347 |
-
table_t = torch.linspace(0.0, 1.0, grid, dtype=torch.float32, device=device)
|
| 348 |
-
table_curve = evaluate(table_t)
|
| 349 |
-
table = ((table_curve.cpu().double() - mean64) @ basis64).to(torch.float32)
|
| 350 |
-
energy = (singular[:rank].square().sum() / singular.square().sum()).item()
|
| 351 |
-
reconstruction = table @ basis.cpu().T + mean.cpu()
|
| 352 |
-
grid_error = ((reconstruction - table_curve.cpu()).norm() / table_curve.cpu().norm()).item()
|
| 353 |
-
# Real runtime values usually lie between table rows. Midpoints provide a
|
| 354 |
-
# sensitive deterministic check of the linear interpolation error.
|
| 355 |
-
mid_t = (torch.arange(grid - 1, dtype=torch.float32, device=device) + 0.5) / (grid - 1)
|
| 356 |
-
mid_curve = evaluate(mid_t).cpu()
|
| 357 |
-
mid_coords = (table[:-1] + table[1:]) * 0.5
|
| 358 |
-
mid_reconstruction = mid_coords @ basis.cpu().T + mean.cpu()
|
| 359 |
-
midpoint_error = ((mid_reconstruction - mid_curve).norm() / mid_curve.norm()).item()
|
| 360 |
-
midpoint_max = ((mid_reconstruction - mid_curve).norm(dim=1) /
|
| 361 |
-
mid_curve.norm(dim=1).clamp(min=1e-12)).max().item()
|
| 362 |
-
projection = torch.cat((basis, mean[:, None]), dim=1)
|
| 363 |
-
del w1, b1, w2, b2, curve, curve_cpu, curve64, centered64, vh, basis64
|
| 364 |
-
del table_curve, reconstruction, mid_curve, mid_reconstruction, mid_coords
|
| 365 |
-
print(f"adaLN curve: rank={rank}, grid={grid}, энергия={energy:.10f}, "
|
| 366 |
-
f"grid err={grid_error:.8%}, midpoint err={midpoint_error:.8%}, "
|
| 367 |
-
f"max={midpoint_max:.8%}")
|
| 368 |
-
return projection, table
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
def make_header(tasks):
|
| 372 |
-
header = {}
|
| 373 |
-
offset = 0
|
| 374 |
-
for item in tasks:
|
| 375 |
-
for key, dtype, shape in item["entries"]:
|
| 376 |
-
size = tensor_bytes(dtype, shape)
|
| 377 |
-
header[key] = {"dtype": dtype, "shape": shape, "data_offsets": [offset, offset + size]}
|
| 378 |
-
offset += size
|
| 379 |
-
raw = json.dumps(header, separators=(",", ":")).encode("utf-8")
|
| 380 |
-
raw += b" " * ((-len(raw)) % 8)
|
| 381 |
-
return raw, offset
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
def raw_bytes(tensor):
|
| 385 |
-
tensor = tensor.detach().to(device="cpu").contiguous()
|
| 386 |
-
if tensor.dtype not in TORCH_TO_ST:
|
| 387 |
-
raise TypeError(f"неподдерживаемый dtype {tensor.dtype}")
|
| 388 |
-
return tensor.view(torch.uint8).numpy().tobytes()
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
def write_tensor(stream, tensor, expected):
|
| 392 |
-
key, dtype, shape = expected
|
| 393 |
-
actual_dtype = TORCH_TO_ST.get(tensor.dtype)
|
| 394 |
-
if actual_dtype != dtype or list(tensor.shape) != shape:
|
| 395 |
-
raise RuntimeError(f"{key}: получили {actual_dtype} {list(tensor.shape)}, ожидалось {dtype} {shape}")
|
| 396 |
-
blob = raw_bytes(tensor)
|
| 397 |
-
expected_bytes = tensor_bytes(dtype, shape)
|
| 398 |
-
if len(blob) != expected_bytes:
|
| 399 |
-
raise RuntimeError(f"{key}: байт {len(blob)}, ожидалось {expected_bytes}")
|
| 400 |
-
stream.write(blob)
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def build(reader, tasks, reference, dst, rank, grid, device, search_min, clip_margin, overwrite):
|
| 404 |
-
partial = dst + ".partial"
|
| 405 |
-
if (os.path.exists(dst) or os.path.exists(partial)) and not overwrite:
|
| 406 |
-
raise SystemExit(f"выход или partial уже существует; используйте --overwrite: {dst}")
|
| 407 |
-
os.makedirs(os.path.dirname(os.path.abspath(dst)), exist_ok=True)
|
| 408 |
-
header, data_size = make_header(tasks)
|
| 409 |
-
projection, table = make_curve(reader, rank, grid, device)
|
| 410 |
-
ratios = build_ratios(search_min)
|
| 411 |
-
t0 = time.time()
|
| 412 |
-
quant_done = curve_done = 0
|
| 413 |
-
expected_quant = sum(item["kind"] in ("quant", "quant_fc1", "quant_qkv") for item in tasks)
|
| 414 |
-
|
| 415 |
-
def report_block(key):
|
| 416 |
-
match = re.fullmatch(r"blocks\.(\d+)\.mlp\.fc2\.weight", key)
|
| 417 |
-
if match:
|
| 418 |
-
print(f" DiT block {int(match.group(1)):02d}/49: INT8 {quant_done}/{expected_quant}, "
|
| 419 |
-
f"{time.time() - t0:.0f} c", flush=True)
|
| 420 |
-
|
| 421 |
-
with safe_open(reference, framework="pt") as ref, open(partial, "wb") as stream:
|
| 422 |
-
stream.write(struct.pack("<Q", len(header)))
|
| 423 |
-
stream.write(header)
|
| 424 |
-
for item in tasks:
|
| 425 |
-
if item["kind"] == "copy":
|
| 426 |
-
tensor = reader.get_tensor(item["source"])
|
| 427 |
-
write_tensor(stream, tensor, item["entries"][0])
|
| 428 |
-
del tensor
|
| 429 |
-
report_block(item["entries"][0][0])
|
| 430 |
-
elif item["kind"] in ("copy_qkv", "quant_qkv"):
|
| 431 |
-
weight = torch.cat([reader.get_tensor(key) for key in item["source"]], dim=0)
|
| 432 |
-
if item["kind"] == "copy_qkv":
|
| 433 |
-
write_tensor(stream, weight, item["entries"][0])
|
| 434 |
-
del weight
|
| 435 |
-
continue
|
| 436 |
-
weight = weight.to(device=device, dtype=torch.float32)
|
| 437 |
-
qdata, scale = quantize_search(weight, item["groupsize"], ratios, clip_margin=clip_margin)
|
| 438 |
-
write_tensor(stream, qdata.cpu(), item["entries"][0])
|
| 439 |
-
write_tensor(stream, scale.cpu(), item["entries"][1])
|
| 440 |
-
blob = torch.tensor(list(item["blob"]), dtype=torch.uint8)
|
| 441 |
-
write_tensor(stream, blob, item["entries"][2])
|
| 442 |
-
quant_done += 1
|
| 443 |
-
del weight, qdata, scale, blob
|
| 444 |
-
elif item["kind"] in ("copy_fc1", "quant_fc1"):
|
| 445 |
-
weight = reader.get_tensor(item["source"])
|
| 446 |
-
half = weight.shape[0] // 2
|
| 447 |
-
weight = torch.cat((weight[half:], weight[:half]), dim=0)
|
| 448 |
-
if item["kind"] == "copy_fc1":
|
| 449 |
-
write_tensor(stream, weight, item["entries"][0])
|
| 450 |
-
del weight
|
| 451 |
-
continue
|
| 452 |
-
weight = weight.to(device=device, dtype=torch.float32)
|
| 453 |
-
qdata, scale = quantize_search(weight, item["groupsize"], ratios, clip_margin=clip_margin)
|
| 454 |
-
write_tensor(stream, qdata.cpu(), item["entries"][0])
|
| 455 |
-
write_tensor(stream, scale.cpu(), item["entries"][1])
|
| 456 |
-
blob = torch.tensor(list(item["blob"]), dtype=torch.uint8)
|
| 457 |
-
write_tensor(stream, blob, item["entries"][2])
|
| 458 |
-
quant_done += 1
|
| 459 |
-
del weight, qdata, scale, blob
|
| 460 |
-
elif item["kind"] == "quant":
|
| 461 |
-
weight = reader.get_tensor(item["source"]).to(device=device, dtype=torch.float32)
|
| 462 |
-
qdata, scale = quantize_search(weight, item["groupsize"], ratios, clip_margin=clip_margin)
|
| 463 |
-
write_tensor(stream, qdata.cpu(), item["entries"][0])
|
| 464 |
-
write_tensor(stream, scale.cpu(), item["entries"][1])
|
| 465 |
-
blob = torch.tensor(list(item["blob"]), dtype=torch.uint8)
|
| 466 |
-
write_tensor(stream, blob, item["entries"][2])
|
| 467 |
-
quant_done += 1
|
| 468 |
-
del weight, qdata, scale, blob
|
| 469 |
-
report_block(item["entries"][0][0])
|
| 470 |
-
elif item["kind"] == "curve":
|
| 471 |
-
source = item["source"]
|
| 472 |
-
weight = reader.get_tensor(source + ".weight").to(device=device, dtype=torch.float32)
|
| 473 |
-
bias = reader.get_tensor(source + ".bias").to(device=device, dtype=torch.float32)
|
| 474 |
-
packed = weight @ projection
|
| 475 |
-
curve_weight = packed[:, :rank]
|
| 476 |
-
curve_bias = bias + packed[:, rank]
|
| 477 |
-
write_tensor(stream, curve_weight.cpu(), item["entries"][0])
|
| 478 |
-
write_tensor(stream, curve_bias.cpu(), item["entries"][1])
|
| 479 |
-
curve_done += 1
|
| 480 |
-
del weight, bias, packed, curve_weight, curve_bias
|
| 481 |
-
elif item["kind"] == "table":
|
| 482 |
-
write_tensor(stream, table.cpu(), item["entries"][0])
|
| 483 |
-
elif item["kind"] == "basis":
|
| 484 |
-
write_tensor(stream, projection[:, :rank].cpu(), item["entries"][0])
|
| 485 |
-
write_tensor(stream, projection[:, rank].cpu(), item["entries"][1])
|
| 486 |
-
elif item["kind"] == "rope":
|
| 487 |
-
write_tensor(stream, ref.get_tensor("rope.inv_freq"), item["entries"][0])
|
| 488 |
-
else:
|
| 489 |
-
raise RuntimeError(item["kind"])
|
| 490 |
-
expected_size = 8 + len(header) + data_size
|
| 491 |
-
actual_size = os.path.getsize(partial)
|
| 492 |
-
if actual_size != expected_size:
|
| 493 |
-
raise RuntimeError(f"partial имеет {actual_size} байт, ожидалось {expected_size}")
|
| 494 |
-
with safe_open(partial, framework="pt") as result:
|
| 495 |
-
expected_keys = sum(len(item["entries"]) for item in tasks)
|
| 496 |
-
if len(result.keys()) != expected_keys:
|
| 497 |
-
raise RuntimeError(f"safetensors открылся, но ключей {len(result.keys())}, ожидалось {expected_keys}")
|
| 498 |
-
os.replace(partial, dst)
|
| 499 |
-
print(f"готово: {dst}\n {actual_size / 2**30:.3f} GiB, INT8={quant_done}, curve={curve_done}, {time.time() - t0:.0f} c")
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
def defaults(variant, rank, grid, time_mode, profile):
|
| 503 |
-
source = os.path.join(HF, "transformer" if variant == "fl2va" else "transformer_ref")
|
| 504 |
-
reference = os.path.join(COMFYORG, f"minimax-h3-{variant}-pruned-int8-convrot-comfyorg.safetensors")
|
| 505 |
-
curve_tag = f"table-k{rank}-g{grid}" if time_mode == "table" else f"dynamic-k{rank}"
|
| 506 |
-
dst = os.path.join(MODELS, f"minimax-h3-{variant}-int8-lean-convrot-{curve_tag}-{profile}.safetensors")
|
| 507 |
-
return source, reference, dst
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
def main():
|
| 511 |
-
ap = argparse.ArgumentParser()
|
| 512 |
-
ap.add_argument("variant", choices=("fl2va", "ref2va"))
|
| 513 |
-
ap.add_argument("--rank", type=int, default=16, choices=(8, 16, 32, 64))
|
| 514 |
-
ap.add_argument("--time-mode", choices=("dynamic", "table"), default="dynamic",
|
| 515 |
-
help="dynamic сохраняет исходный F32 time_embedder; table совместим с pruned")
|
| 516 |
-
ap.add_argument("--grid", type=int, default=4097,
|
| 517 |
-
help="число строк table или плотность численной проверки dynamic basis")
|
| 518 |
-
ap.add_argument("--profile", choices=tuple(QUALITY_PROFILES), default="quality21")
|
| 519 |
-
ap.add_argument("--bf16-layer", action="append", default=[],
|
| 520 |
-
help="дополнительный base-name основного слоя, например blocks.48.mlp.fc2")
|
| 521 |
-
ap.add_argument("--groupsize", type=int, choices=(16, 64, 256), default=256)
|
| 522 |
-
ap.add_argument("--src", default="")
|
| 523 |
-
ap.add_argument("--reference", default="")
|
| 524 |
-
ap.add_argument("--dst", default="")
|
| 525 |
-
ap.add_argument("--device", default="cuda:0")
|
| 526 |
-
ap.add_argument("--search-min", type=float, default=0.80)
|
| 527 |
-
ap.add_argument("--clip-margin", type=float, default=0.05)
|
| 528 |
-
ap.add_argument("--dry-run", action="store_true")
|
| 529 |
-
ap.add_argument("--overwrite", action="store_true")
|
| 530 |
-
args = ap.parse_args()
|
| 531 |
-
if args.grid < 2:
|
| 532 |
-
ap.error("--grid должен быть не меньше 2")
|
| 533 |
-
|
| 534 |
-
bf16_layers = set(QUALITY_PROFILES[args.profile]) | set(args.bf16_layer)
|
| 535 |
-
source, reference, dst = defaults(args.variant, args.rank, args.grid, args.time_mode, args.profile)
|
| 536 |
-
source = args.src or source
|
| 537 |
-
reference = args.reference or reference
|
| 538 |
-
dst = args.dst or dst
|
| 539 |
-
print(f"variant={args.variant} rank={args.rank} time={args.time_mode} profile={args.profile}\n"
|
| 540 |
-
f"BF16 main={sorted(bf16_layers)}\nsrc={source}\nreference={reference}\ndst={dst}")
|
| 541 |
-
reader = ShardReader(source)
|
| 542 |
-
try:
|
| 543 |
-
validate_source(reader)
|
| 544 |
-
tasks = build_tasks(reader, args.rank, args.grid, args.time_mode, bf16_layers, args.groupsize)
|
| 545 |
-
inspect_reference(tasks, reference, args.time_mode, bf16_layers)
|
| 546 |
-
_, data_size = make_header(tasks)
|
| 547 |
-
entries = sum(len(item["entries"]) for item in tasks)
|
| 548 |
-
quant_count = sum(item["kind"] in ("quant", "quant_fc1", "quant_qkv") for item in tasks)
|
| 549 |
-
print(f"план: {entries} тензоров, {quant_count} INT8 g{args.groupsize}, "
|
| 550 |
-
f"BF16 main={len(bf16_layers)}, 51 adaLN curve F32 k={args.rank} time={args.time_mode}; "
|
| 551 |
-
f"данные {data_size / 2**30:.3f} GiB")
|
| 552 |
-
if args.dry_run:
|
| 553 |
-
print("dry-run: веса не читались целиком, GPU не использовалась, файл не создан")
|
| 554 |
-
return
|
| 555 |
-
build(reader, tasks, reference, dst, args.rank, args.grid, args.device, args.search_min,
|
| 556 |
-
args.clip_margin, args.overwrite)
|
| 557 |
-
finally:
|
| 558 |
-
reader.close()
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
if __name__ == "__main__":
|
| 562 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/mm_validate_dynamic_built.py
DELETED
|
@@ -1,137 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""Validate a built MiniMax-H3 dynamic-basis checkpoint against raw HF weights."""
|
| 3 |
-
import argparse
|
| 4 |
-
import json
|
| 5 |
-
import math
|
| 6 |
-
import os
|
| 7 |
-
import sys
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
-
import torch.nn.functional as F
|
| 11 |
-
from safetensors import safe_open
|
| 12 |
-
|
| 13 |
-
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
-
ROOT = os.environ.get("MINIMAX_H3_ROOT", os.getcwd())
|
| 15 |
-
sys.path.insert(0, SCRIPT_DIR)
|
| 16 |
-
from mm_quantize_lean import QUALITY_PROFILES, ShardReader # noqa: E402
|
| 17 |
-
|
| 18 |
-
sys.stdout.reconfigure(encoding="utf-8")
|
| 19 |
-
torch.set_grad_enabled(False)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def rel(actual, expected):
|
| 23 |
-
return ((actual - expected).norm() / expected.norm().clamp(min=1e-12)).item()
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def main():
|
| 27 |
-
ap = argparse.ArgumentParser()
|
| 28 |
-
ap.add_argument("variant", choices=("fl2va", "ref2va"))
|
| 29 |
-
ap.add_argument("checkpoint")
|
| 30 |
-
ap.add_argument("--src", default="", help="raw diffusers transformer directory")
|
| 31 |
-
ap.add_argument("--profile", choices=tuple(QUALITY_PROFILES), default="quality21")
|
| 32 |
-
ap.add_argument("--samples", type=int, default=9)
|
| 33 |
-
args = ap.parse_args()
|
| 34 |
-
|
| 35 |
-
source = args.src or os.path.join(ROOT, "HF",
|
| 36 |
-
"transformer" if args.variant == "fl2va" else "transformer_ref")
|
| 37 |
-
expected_bf16 = set(QUALITY_PROFILES[args.profile])
|
| 38 |
-
results = {}
|
| 39 |
-
|
| 40 |
-
with ShardReader(source) as raw, safe_open(args.checkpoint, framework="pt") as built:
|
| 41 |
-
keys = set(built.keys())
|
| 42 |
-
quant = [key for key in keys if key.endswith(".comfy_quant")]
|
| 43 |
-
groups = {}
|
| 44 |
-
for key in quant:
|
| 45 |
-
config = json.loads(bytes(built.get_tensor(key).numpy()).decode())
|
| 46 |
-
groups[config["convrot_groupsize"]] = groups.get(config["convrot_groupsize"], 0) + 1
|
| 47 |
-
|
| 48 |
-
actual_bf16 = set()
|
| 49 |
-
for block in range(50):
|
| 50 |
-
for suffix in ("attn.qkv_proj", "attn.out_proj", "mlp.fc1", "mlp.fc2"):
|
| 51 |
-
base = f"blocks.{block}.{suffix}"
|
| 52 |
-
if built.get_slice(base + ".weight").get_dtype() == "BF16":
|
| 53 |
-
actual_bf16.add(base)
|
| 54 |
-
if actual_bf16 != expected_bf16:
|
| 55 |
-
raise RuntimeError(f"BF16 profile mismatch: -{sorted(expected_bf16 - actual_bf16)} "
|
| 56 |
-
f"+{sorted(actual_bf16 - expected_bf16)}")
|
| 57 |
-
|
| 58 |
-
time_pairs = (
|
| 59 |
-
("time_embedder.linear_1.weight", "time_embedder.proj_in.weight"),
|
| 60 |
-
("time_embedder.linear_1.bias", "time_embedder.proj_in.bias"),
|
| 61 |
-
("time_embedder.linear_2.weight", "time_embedder.proj_out.weight"),
|
| 62 |
-
("time_embedder.linear_2.bias", "time_embedder.proj_out.bias"),
|
| 63 |
-
)
|
| 64 |
-
time_bitwise = all(torch.equal(raw.get_tensor(src), built.get_tensor(dst))
|
| 65 |
-
for src, dst in time_pairs)
|
| 66 |
-
if not time_bitwise:
|
| 67 |
-
raise RuntimeError("time_embedder differs from raw HF")
|
| 68 |
-
|
| 69 |
-
# All quality21 BF16 islands are out_proj or fc2 and should be exact copies.
|
| 70 |
-
bf16_bad = []
|
| 71 |
-
for base in sorted(actual_bf16):
|
| 72 |
-
parts = base.split(".")
|
| 73 |
-
block = int(parts[1])
|
| 74 |
-
suffix = ".".join(parts[2:])
|
| 75 |
-
if suffix == "attn.out_proj":
|
| 76 |
-
src = f"transformer_blocks.{block}.attn.to_out.0.weight"
|
| 77 |
-
elif suffix == "mlp.fc2":
|
| 78 |
-
src = f"transformer_blocks.{block}.ff.net.2.weight"
|
| 79 |
-
else:
|
| 80 |
-
raise RuntimeError(f"validator needs mapping for {base}")
|
| 81 |
-
if not torch.equal(raw.get_tensor(src), built.get_tensor(base + ".weight")):
|
| 82 |
-
bf16_bad.append(base)
|
| 83 |
-
if bf16_bad:
|
| 84 |
-
raise RuntimeError(f"BF16 tensors differ: {bf16_bad}")
|
| 85 |
-
|
| 86 |
-
w1 = built.get_tensor("time_embedder.proj_in.weight").float()
|
| 87 |
-
b1 = built.get_tensor("time_embedder.proj_in.bias").float()
|
| 88 |
-
w2 = built.get_tensor("time_embedder.proj_out.weight").float()
|
| 89 |
-
b2 = built.get_tensor("time_embedder.proj_out.bias").float()
|
| 90 |
-
basis = built.get_tensor("adaln_curve_basis").float()
|
| 91 |
-
mean = built.get_tensor("adaln_curve_mean").float()
|
| 92 |
-
orth_error = (basis.T @ basis - torch.eye(basis.shape[1])).abs().max().item()
|
| 93 |
-
|
| 94 |
-
t = torch.linspace(0.0, 1.0, args.samples, dtype=torch.float32)
|
| 95 |
-
half = 128
|
| 96 |
-
freqs = torch.exp(-math.log(10000.0) * torch.arange(half, dtype=torch.float32) / half)
|
| 97 |
-
phase = t[:, None] * freqs[None]
|
| 98 |
-
sinusoid = torch.cat((torch.cos(phase), torch.sin(phase)), dim=1)
|
| 99 |
-
u = F.silu(F.linear(F.silu(F.linear(sinusoid, w1, b1)), w2, b2))
|
| 100 |
-
coords = (u - mean) @ basis
|
| 101 |
-
|
| 102 |
-
curve_errors = {}
|
| 103 |
-
for block in (0, 24, 49):
|
| 104 |
-
src = f"transformer_blocks.{block}.adaln_proj.linear"
|
| 105 |
-
dst = f"blocks.{block}.adaln_proj.linear"
|
| 106 |
-
exact = F.linear(u, raw.get_tensor(src + ".weight").float(),
|
| 107 |
-
raw.get_tensor(src + ".bias").float())
|
| 108 |
-
approx = F.linear(coords, built.get_tensor(dst + ".weight").float(),
|
| 109 |
-
built.get_tensor(dst + ".bias").float())
|
| 110 |
-
curve_errors[f"block_{block}"] = rel(approx, exact)
|
| 111 |
-
del exact, approx
|
| 112 |
-
exact = F.linear(u, raw.get_tensor("norm_out.linear.weight").float(),
|
| 113 |
-
raw.get_tensor("norm_out.linear.bias").float())
|
| 114 |
-
approx = F.linear(coords, built.get_tensor("final_layer.adaln_proj.linear.weight").float(),
|
| 115 |
-
built.get_tensor("final_layer.adaln_proj.linear.bias").float())
|
| 116 |
-
curve_errors["final"] = rel(approx, exact)
|
| 117 |
-
|
| 118 |
-
results = {
|
| 119 |
-
"checkpoint": args.checkpoint,
|
| 120 |
-
"bytes": os.path.getsize(args.checkpoint),
|
| 121 |
-
"keys": len(keys),
|
| 122 |
-
"quant": len(quant),
|
| 123 |
-
"quant_groups": groups,
|
| 124 |
-
"bf16_main": len(actual_bf16),
|
| 125 |
-
"time_embedder_bitwise": time_bitwise,
|
| 126 |
-
"bf16_main_bitwise": len(actual_bf16) - len(bf16_bad),
|
| 127 |
-
"basis_shape": list(basis.shape),
|
| 128 |
-
"basis_orthogonality_max_abs": orth_error,
|
| 129 |
-
"curve_samples": args.samples,
|
| 130 |
-
"adaln_relative_errors": curve_errors,
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
print(json.dumps(results, ensure_ascii=False, indent=2))
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
if __name__ == "__main__":
|
| 137 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|