Upload folder using huggingface_hub
Browse files- adapter_stage2_best.safetensors +3 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__init__.py +26 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/__init__.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/__init__.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/adapter_clip.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/adapter_clip.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/export_cond.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/export_cond.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_dequant.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_dequant.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_qwen35.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_qwen35.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/nodes.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/nodes.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/student.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/student.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__init__.py +3 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/__init__.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/__init__.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/model.cpython-311.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/model.cpython-313.pyc +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/model.py +230 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/model.py.bak_old +160 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter_clip.py +132 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/check_alignment.py +119 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/export_adapter.py +43 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/export_cond.py +42 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/gguf_dequant.py +309 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/gguf_qwen35.py +445 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/inspect_gguf.py +66 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/nodes.py +129 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/qwen35_config.json +104 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/student.py +161 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/test_fake_clip.py +160 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/test_gguf_loader.py +212 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/merges.txt +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/tokenizer.json +0 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/tokenizer_config.json +246 -0
- custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/vocab.json +0 -0
adapter_stage2_best.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6559e73b3f1b8dd3d3b8702ba4f6221c2bf68427f39741db3cafef53dfe4af5c
|
| 3 |
+
size 2288170738
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
import folder_paths
|
| 4 |
+
|
| 5 |
+
# models/minimax_h3_adapter/ -> adapter_stage2.safetensors
|
| 6 |
+
folder_paths.add_model_folder_path("minimax_h3_adapter",
|
| 7 |
+
os.path.join(folder_paths.models_dir, "minimax_h3_adapter"))
|
| 8 |
+
# models/text_encoders/ -> Qwen3.5-4B 文件夹(与 minimax CLIP 同目录)
|
| 9 |
+
folder_paths.add_model_folder_path("minimax_h3_student",
|
| 10 |
+
os.path.join(folder_paths.models_dir, "text_encoders"))
|
| 11 |
+
|
| 12 |
+
from .nodes import MiniMaxH3AdapterLoader, MiniMaxH3AdapterFromCLIPLoader # noqa: E402
|
| 13 |
+
from .export_cond import MiniMaxH3ExportCond # noqa: E402
|
| 14 |
+
|
| 15 |
+
NODE_CLASS_MAPPINGS = {
|
| 16 |
+
"MiniMaxH3AdapterLoader": MiniMaxH3AdapterLoader,
|
| 17 |
+
"MiniMaxH3AdapterFromCLIPLoader": MiniMaxH3AdapterFromCLIPLoader,
|
| 18 |
+
"MiniMaxH3ExportCond": MiniMaxH3ExportCond,
|
| 19 |
+
}
|
| 20 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 21 |
+
"MiniMaxH3AdapterLoader": "MiniMax H3 Adapter Loader",
|
| 22 |
+
"MiniMaxH3AdapterFromCLIPLoader": "MiniMax H3 Adapter (From CLIP)",
|
| 23 |
+
"MiniMaxH3ExportCond": "MiniMax H3 Export Cond (Debug)",
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (1.1 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (978 Bytes). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/adapter_clip.cpython-311.pyc
ADDED
|
Binary file (9.26 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/adapter_clip.cpython-313.pyc
ADDED
|
Binary file (8.65 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/export_cond.cpython-311.pyc
ADDED
|
Binary file (3.29 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/export_cond.cpython-313.pyc
ADDED
|
Binary file (2.94 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_dequant.cpython-311.pyc
ADDED
|
Binary file (25.8 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_dequant.cpython-313.pyc
ADDED
|
Binary file (24 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_qwen35.cpython-311.pyc
ADDED
|
Binary file (32.4 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/gguf_qwen35.cpython-313.pyc
ADDED
|
Binary file (28.5 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/nodes.cpython-311.pyc
ADDED
|
Binary file (9.77 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/nodes.cpython-313.pyc
ADDED
|
Binary file (8.51 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/student.cpython-311.pyc
ADDED
|
Binary file (12.1 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/__pycache__/student.cpython-313.pyc
ADDED
|
Binary file (10.9 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .model import H3Adapter, H3_HIDDEN, STUDENT_HIDDEN
|
| 2 |
+
|
| 3 |
+
__all__ = ["H3Adapter", "H3_HIDDEN", "STUDENT_HIDDEN"]
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (334 Bytes). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (302 Bytes). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/model.cpython-311.pyc
ADDED
|
Binary file (20.6 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/__pycache__/model.cpython-313.pyc
ADDED
|
Binary file (13 kB). View file
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/model.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""H3 文本编码器替换适配器(接口蒸馏):source_projection + CrossAttention Resampler + TokenRefiner。
|
| 2 |
+
|
| 3 |
+
前向:
|
| 4 |
+
h3_ids [B, S_T] --QueryEmbedding--> Q [B, S_T, 5376]
|
| 5 |
+
student_hidden [B, S_S, 2560] --source_projection--> KV [B, S_S, 5376]
|
| 6 |
+
CrossAttentionBlock(Q, KV) -> [B, S_T, 5376]
|
| 7 |
+
TokenRefiner(2 层, 原权重初始化) -> [B, S_T, 5376] # 与教师 target 同坐标系
|
| 8 |
+
|
| 9 |
+
TokenRefiner 结构严格复刻原始 checkpoint(已源码核实):
|
| 10 |
+
- fused qkv (chunk(3)) + per-head qk_norm + 双向注意力 + out_proj,全部 bias=False
|
| 11 |
+
- SwiGLU MLP: fc1 为 fused [gate; value](gate 在前),fc2(silu(gate)*value)
|
| 12 |
+
- 2 个 pre-norm block + final RMSNorm,eps 全部 1e-5
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn as nn
|
| 20 |
+
import torch.nn.functional as F
|
| 21 |
+
|
| 22 |
+
H3_VOCAB = 151936
|
| 23 |
+
H3_HIDDEN = 5376
|
| 24 |
+
REFINER_HEADS = 56
|
| 25 |
+
REFINER_HEAD_DIM = 128
|
| 26 |
+
REFINER_FFN = 14336
|
| 27 |
+
STUDENT_HIDDEN = 2560
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class RMSNorm(nn.Module):
|
| 31 |
+
def __init__(self, dim: int, eps: float = 1e-5):
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 34 |
+
self.eps = eps
|
| 35 |
+
|
| 36 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 37 |
+
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class SwiGLUFFN(nn.Module):
|
| 41 |
+
def __init__(self, hidden: int, ffn: int):
|
| 42 |
+
super().__init__()
|
| 43 |
+
self.fc1 = nn.Linear(hidden, 2 * ffn, bias=False)
|
| 44 |
+
self.fc2 = nn.Linear(ffn, hidden, bias=False)
|
| 45 |
+
|
| 46 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 47 |
+
gate, value = self.fc1(x).chunk(2, dim=-1)
|
| 48 |
+
return self.fc2(F.silu(gate) * value)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class TokenRefinerAttention(nn.Module):
|
| 52 |
+
def __init__(self, hidden: int, heads: int, dim_head: int):
|
| 53 |
+
super().__init__()
|
| 54 |
+
self.heads = heads
|
| 55 |
+
self.head_dim = dim_head
|
| 56 |
+
self.inner_dim = heads * dim_head
|
| 57 |
+
self.qkv_proj = nn.Linear(hidden, 3 * self.inner_dim, bias=False)
|
| 58 |
+
self.q_norm = RMSNorm(dim_head)
|
| 59 |
+
self.k_norm = RMSNorm(dim_head)
|
| 60 |
+
self.out_proj = nn.Linear(self.inner_dim, hidden, bias=False)
|
| 61 |
+
self.use_sdpa = True # SDPA flash: 注意力内存 O(S²)->O(S),数值与手写注意力差 ~1e-3(蒸馏噪声级)
|
| 62 |
+
|
| 63 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 64 |
+
q, k, v = self.qkv_proj(x).chunk(3, dim=-1)
|
| 65 |
+
q = self.q_norm(q.unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2))
|
| 66 |
+
k = self.k_norm(k.unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2))
|
| 67 |
+
v = v.unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 68 |
+
if self.use_sdpa:
|
| 69 |
+
out = F.scaled_dot_product_attention(q, k, v, scale=self.head_dim ** -0.5)
|
| 70 |
+
else:
|
| 71 |
+
attn = torch.softmax((q @ k.transpose(-2, -1)) * (self.head_dim ** -0.5), dim=-1)
|
| 72 |
+
out = attn @ v
|
| 73 |
+
return self.out_proj(out.transpose(1, 2).flatten(2))
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class TokenRefinerBlock(nn.Module):
|
| 77 |
+
def __init__(self, hidden: int, heads: int, dim_head: int, ffn: int):
|
| 78 |
+
super().__init__()
|
| 79 |
+
self.norm1 = RMSNorm(hidden)
|
| 80 |
+
self.attn = TokenRefinerAttention(hidden, heads, dim_head)
|
| 81 |
+
self.norm2 = RMSNorm(hidden)
|
| 82 |
+
self.mlp = SwiGLUFFN(hidden, ffn)
|
| 83 |
+
|
| 84 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 85 |
+
x = x + self.attn(self.norm1(x))
|
| 86 |
+
x = x + self.mlp(self.norm2(x))
|
| 87 |
+
return x
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class TokenRefiner(nn.Module):
|
| 91 |
+
def __init__(self, num_layers: int = 2, hidden: int = H3_HIDDEN,
|
| 92 |
+
heads: int = REFINER_HEADS, dim_head: int = REFINER_HEAD_DIM, ffn: int = REFINER_FFN):
|
| 93 |
+
super().__init__()
|
| 94 |
+
self.blocks = nn.ModuleList([TokenRefinerBlock(hidden, heads, dim_head, ffn) for _ in range(num_layers)])
|
| 95 |
+
self.final_norm = RMSNorm(hidden)
|
| 96 |
+
|
| 97 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 98 |
+
for block in self.blocks:
|
| 99 |
+
x = block(x)
|
| 100 |
+
return self.final_norm(x)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class CrossAttentionBlock(nn.Module):
|
| 104 |
+
"""跨空间交叉注意(学生 2560 投影空间 <-> 教师 5376 表示空间)。
|
| 105 |
+
|
| 106 |
+
训练稳定性升级(兼容旧权重加载):
|
| 107 |
+
- QK-norm: to_q/to_k 投影后对每头做 RMSNorm(Llama 3.2 vision 交叉注意惯例,
|
| 108 |
+
稳定跨空间注意力的 q/k 尺度失配)
|
| 109 |
+
- gated tanh: out_proj 输出经 tanh(gate) 门控(Flamingo 惯例),gate 初始 0 => 恒等
|
| 110 |
+
- attn dropout: 交叉注意 dropout 0.1(Emu3 防后期 collapse)
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
def __init__(self, hidden: int = H3_HIDDEN, heads: int = 32, dim_head: int = 128, ffn: int = REFINER_FFN,
|
| 114 |
+
attn_dropout: float = 0.1, use_qk_norm: bool = True, use_gate: bool = True):
|
| 115 |
+
super().__init__()
|
| 116 |
+
self.heads = heads
|
| 117 |
+
self.head_dim = dim_head
|
| 118 |
+
self.inner_dim = heads * dim_head
|
| 119 |
+
self.norm_q = RMSNorm(hidden)
|
| 120 |
+
self.norm_kv = RMSNorm(hidden)
|
| 121 |
+
self.to_q = nn.Linear(hidden, self.inner_dim, bias=False)
|
| 122 |
+
self.to_k = nn.Linear(hidden, self.inner_dim, bias=False)
|
| 123 |
+
self.to_v = nn.Linear(hidden, self.inner_dim, bias=False)
|
| 124 |
+
self.out_proj = nn.Linear(self.inner_dim, hidden, bias=False)
|
| 125 |
+
self.norm2 = RMSNorm(hidden)
|
| 126 |
+
self.mlp = SwiGLUFFN(hidden, ffn)
|
| 127 |
+
self.use_sdpa = True
|
| 128 |
+
self.attn_dropout = attn_dropout
|
| 129 |
+
self.use_qk_norm = use_qk_norm
|
| 130 |
+
if use_qk_norm:
|
| 131 |
+
self.qk_norm = RMSNorm(dim_head)
|
| 132 |
+
self.use_gate = use_gate
|
| 133 |
+
if use_gate:
|
| 134 |
+
self.gate = nn.Parameter(torch.zeros(1)) # tanh(0)=0 -> 恒等,兼容旧权重
|
| 135 |
+
|
| 136 |
+
def forward(self, q: torch.Tensor, kv: torch.Tensor) -> torch.Tensor:
|
| 137 |
+
qn = self.norm_q(q)
|
| 138 |
+
kvn = self.norm_kv(kv)
|
| 139 |
+
qh = self.to_q(qn).unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 140 |
+
kh = self.to_k(kvn).unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 141 |
+
vh = self.to_v(kvn).unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 142 |
+
if self.use_qk_norm:
|
| 143 |
+
qh = self.qk_norm(qh)
|
| 144 |
+
kh = self.qk_norm(kh)
|
| 145 |
+
if self.use_sdpa:
|
| 146 |
+
out = F.scaled_dot_product_attention(qh, kh, vh, scale=self.head_dim ** -0.5,
|
| 147 |
+
dropout_p=self.attn_dropout if self.training else 0.0)
|
| 148 |
+
else:
|
| 149 |
+
attn = torch.softmax((qh @ kh.transpose(-2, -1)) * (self.head_dim ** -0.5), dim=-1)
|
| 150 |
+
out = attn @ vh
|
| 151 |
+
proj = self.out_proj(out.transpose(1, 2).flatten(2))
|
| 152 |
+
x = q + (torch.tanh(self.gate) * proj if self.use_gate else proj)
|
| 153 |
+
x = x + self.mlp(self.norm2(x))
|
| 154 |
+
return x
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class QueryEmbedding(nn.Module):
|
| 158 |
+
def __init__(self, vocab: int = H3_VOCAB, dim: int = 256, out: int = H3_HIDDEN):
|
| 159 |
+
super().__init__()
|
| 160 |
+
self.embed = nn.Embedding(vocab, dim)
|
| 161 |
+
self.proj = nn.Linear(dim, out, bias=True)
|
| 162 |
+
|
| 163 |
+
def forward(self, ids: torch.Tensor) -> torch.Tensor:
|
| 164 |
+
return self.proj(self.embed(ids))
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class H3Adapter(nn.Module):
|
| 168 |
+
"""完整适配器。param 约 1.14B(source_proj 13.8M + query_embed 40.3M + crossattn 319M + refiner 751M)。"""
|
| 169 |
+
|
| 170 |
+
def __init__(self):
|
| 171 |
+
super().__init__()
|
| 172 |
+
self.source_projection = nn.Linear(STUDENT_HIDDEN, H3_HIDDEN, bias=True)
|
| 173 |
+
self.query_embedding = QueryEmbedding()
|
| 174 |
+
self.cross_attention = CrossAttentionBlock()
|
| 175 |
+
self.token_refiner = TokenRefiner()
|
| 176 |
+
|
| 177 |
+
def forward(self, h3_ids: torch.Tensor, student_hidden: torch.Tensor) -> torch.Tensor:
|
| 178 |
+
kv = self.source_projection(student_hidden)
|
| 179 |
+
q = self.query_embedding(h3_ids)
|
| 180 |
+
x = self.cross_attention(q, kv)
|
| 181 |
+
x = self.token_refiner(x)
|
| 182 |
+
return x
|
| 183 |
+
|
| 184 |
+
def load_token_refiner(self, state_dict: dict[str, torch.Tensor], strict: bool = True) -> None:
|
| 185 |
+
refiner_state = {
|
| 186 |
+
k[len("token_refiner."):]: v
|
| 187 |
+
for k, v in state_dict.items()
|
| 188 |
+
if k.startswith("token_refiner.")
|
| 189 |
+
}
|
| 190 |
+
missing, unexpected = self.token_refiner.load_state_dict(refiner_state, strict=strict)
|
| 191 |
+
assert not missing and not unexpected, f"refiner load: missing={missing} unexpected={unexpected}"
|
| 192 |
+
|
| 193 |
+
def trainable_modules(self, stage: int) -> list[nn.Parameter]:
|
| 194 |
+
if stage == 1:
|
| 195 |
+
return (
|
| 196 |
+
list(self.source_projection.parameters())
|
| 197 |
+
+ list(self.query_embedding.parameters())
|
| 198 |
+
+ list(self.cross_attention.parameters())
|
| 199 |
+
)
|
| 200 |
+
return list(self.parameters())
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class TeacherHead(nn.Module):
|
| 204 |
+
"""教师 target 计算: h50 -> condition_proj(5120->5376) -> token_refiner -> [B, S_T, 5376]。"""
|
| 205 |
+
|
| 206 |
+
def __init__(self):
|
| 207 |
+
super().__init__()
|
| 208 |
+
self.condition_proj = nn.Linear(5120, H3_HIDDEN, bias=True)
|
| 209 |
+
self.token_refiner = TokenRefiner()
|
| 210 |
+
|
| 211 |
+
def forward(self, h50: torch.Tensor) -> torch.Tensor:
|
| 212 |
+
return self.token_refiner(self.condition_proj(h50))
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def compute_query_embed_init(embed_tokens: torch.Tensor, dim: int = 256, seed: int = 0) -> torch.Tensor:
|
| 216 |
+
"""query embedding 初始化: embed.weight = E @ P,P 为 5120->dim 的随机投影。
|
| 217 |
+
|
| 218 |
+
仅用于一次性初始化,后续可训练。embed_tokens 为 [151936, 5120](BF16 或 FP32)。
|
| 219 |
+
"""
|
| 220 |
+
assert embed_tokens.dim() == 2 and embed_tokens.shape[1] == 5120, embed_tokens.shape
|
| 221 |
+
rng = torch.Generator().manual_seed(seed)
|
| 222 |
+
p = torch.randn(5120, dim, generator=rng) * (1.0 / math.sqrt(5120))
|
| 223 |
+
embed_tokens = embed_tokens.to(torch.float32)
|
| 224 |
+
chunks = 8
|
| 225 |
+
out = torch.empty(embed_tokens.shape[0], dim, dtype=torch.float32)
|
| 226 |
+
for i in range(chunks):
|
| 227 |
+
lo = i * embed_tokens.shape[0] // chunks
|
| 228 |
+
hi = (i + 1) * embed_tokens.shape[0] // chunks
|
| 229 |
+
out[lo:hi] = embed_tokens[lo:hi] @ p
|
| 230 |
+
return out.to(torch.bfloat16)
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter/model.py.bak_old
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""H3 文本编码器替换适配器(接口蒸馏)——ComfyUI 节点用自包含版。
|
| 2 |
+
|
| 3 |
+
前向:
|
| 4 |
+
h3_ids [B, S_T] --QueryEmbedding--> Q [B, S_T, 5376]
|
| 5 |
+
student_hidden [B, S_S, 2560] --source_projection--> KV [B, S_S, 5376]
|
| 6 |
+
CrossAttentionBlock(Q, KV) -> [B, S_T, 5376]
|
| 7 |
+
TokenRefiner(2 层, 原权重初始化) -> [B, S_T, 5376] # 与教师 target 同坐标系
|
| 8 |
+
|
| 9 |
+
TokenRefiner 结构严格复刻原始 checkpoint:
|
| 10 |
+
- fused qkv (chunk(3)) + per-head qk_norm + 双向注意力 + out_proj,全部 bias=False
|
| 11 |
+
- SwiGLU MLP: fc1 为 fused [gate; value](gate 在前),fc2(silu(gate)*value)
|
| 12 |
+
- 2 个 pre-norm block + final RMSNorm,eps 全部 1e-5
|
| 13 |
+
|
| 14 |
+
注意力 use_sdpa=True(默认): SDPA flash 省显存;手写注意力保留为回退。
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn as nn
|
| 20 |
+
import torch.nn.functional as F
|
| 21 |
+
|
| 22 |
+
H3_VOCAB = 151936
|
| 23 |
+
H3_HIDDEN = 5376
|
| 24 |
+
REFINER_HEADS = 56
|
| 25 |
+
REFINER_HEAD_DIM = 128
|
| 26 |
+
REFINER_FFN = 14336
|
| 27 |
+
STUDENT_HIDDEN = 2560
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class RMSNorm(nn.Module):
|
| 31 |
+
def __init__(self, hidden: int, eps: float = 1e-5):
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.weight = nn.Parameter(torch.ones(hidden))
|
| 34 |
+
self.eps = eps
|
| 35 |
+
|
| 36 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 37 |
+
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class SwiGLUFFN(nn.Module):
|
| 41 |
+
def __init__(self, hidden: int, ffn: int):
|
| 42 |
+
super().__init__()
|
| 43 |
+
self.fc1 = nn.Linear(hidden, 2 * ffn, bias=False)
|
| 44 |
+
self.fc2 = nn.Linear(ffn, hidden, bias=False)
|
| 45 |
+
|
| 46 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 47 |
+
gate, value = self.fc1(x).chunk(2, dim=-1)
|
| 48 |
+
return self.fc2(F.silu(gate) * value)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class TokenRefinerAttention(nn.Module):
|
| 52 |
+
def __init__(self, hidden: int, heads: int, dim_head: int):
|
| 53 |
+
super().__init__()
|
| 54 |
+
self.heads = heads
|
| 55 |
+
self.head_dim = dim_head
|
| 56 |
+
self.inner_dim = heads * dim_head
|
| 57 |
+
self.qkv_proj = nn.Linear(hidden, 3 * self.inner_dim, bias=False)
|
| 58 |
+
self.q_norm = RMSNorm(dim_head)
|
| 59 |
+
self.k_norm = RMSNorm(dim_head)
|
| 60 |
+
self.out_proj = nn.Linear(self.inner_dim, hidden, bias=False)
|
| 61 |
+
self.use_sdpa = True # SDPA flash: 注意力内存 O(S²)->O(S),数值与手写注意力差 ~1e-3(蒸馏噪声级)
|
| 62 |
+
|
| 63 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 64 |
+
q, k, v = self.qkv_proj(x).chunk(3, dim=-1)
|
| 65 |
+
q = self.q_norm(q.unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2))
|
| 66 |
+
k = self.k_norm(k.unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2))
|
| 67 |
+
v = v.unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 68 |
+
if self.use_sdpa:
|
| 69 |
+
out = F.scaled_dot_product_attention(q, k, v, scale=self.head_dim ** -0.5)
|
| 70 |
+
else:
|
| 71 |
+
attn = torch.softmax((q @ k.transpose(-2, -1)) * (self.head_dim ** -0.5), dim=-1)
|
| 72 |
+
out = attn @ v
|
| 73 |
+
return self.out_proj(out.transpose(1, 2).flatten(2))
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class TokenRefinerBlock(nn.Module):
|
| 77 |
+
def __init__(self, hidden: int, heads: int, dim_head: int, ffn: int):
|
| 78 |
+
super().__init__()
|
| 79 |
+
self.norm1 = RMSNorm(hidden)
|
| 80 |
+
self.attn = TokenRefinerAttention(hidden, heads, dim_head)
|
| 81 |
+
self.norm2 = RMSNorm(hidden)
|
| 82 |
+
self.mlp = SwiGLUFFN(hidden, ffn)
|
| 83 |
+
|
| 84 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 85 |
+
x = x + self.attn(self.norm1(x))
|
| 86 |
+
x = x + self.mlp(self.norm2(x))
|
| 87 |
+
return x
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class TokenRefiner(nn.Module):
|
| 91 |
+
def __init__(self, num_layers: int = 2, hidden: int = H3_HIDDEN,
|
| 92 |
+
heads: int = REFINER_HEADS, dim_head: int = REFINER_HEAD_DIM, ffn: int = REFINER_FFN):
|
| 93 |
+
super().__init__()
|
| 94 |
+
self.blocks = nn.ModuleList([TokenRefinerBlock(hidden, heads, dim_head, ffn) for _ in range(num_layers)])
|
| 95 |
+
self.final_norm = RMSNorm(hidden)
|
| 96 |
+
|
| 97 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 98 |
+
for block in self.blocks:
|
| 99 |
+
x = block(x)
|
| 100 |
+
return self.final_norm(x)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class CrossAttentionBlock(nn.Module):
|
| 104 |
+
def __init__(self, hidden: int = H3_HIDDEN, heads: int = 32, dim_head: int = 128, ffn: int = REFINER_FFN):
|
| 105 |
+
super().__init__()
|
| 106 |
+
self.heads = heads
|
| 107 |
+
self.head_dim = dim_head
|
| 108 |
+
self.inner_dim = heads * dim_head
|
| 109 |
+
self.norm_q = RMSNorm(hidden)
|
| 110 |
+
self.norm_kv = RMSNorm(hidden)
|
| 111 |
+
self.to_q = nn.Linear(hidden, self.inner_dim, bias=False)
|
| 112 |
+
self.to_k = nn.Linear(hidden, self.inner_dim, bias=False)
|
| 113 |
+
self.to_v = nn.Linear(hidden, self.inner_dim, bias=False)
|
| 114 |
+
self.out_proj = nn.Linear(self.inner_dim, hidden, bias=False)
|
| 115 |
+
self.norm2 = RMSNorm(hidden)
|
| 116 |
+
self.mlp = SwiGLUFFN(hidden, ffn)
|
| 117 |
+
self.use_sdpa = True
|
| 118 |
+
|
| 119 |
+
def forward(self, q: torch.Tensor, kv: torch.Tensor) -> torch.Tensor:
|
| 120 |
+
qn = self.norm_q(q)
|
| 121 |
+
kvn = self.norm_kv(kv)
|
| 122 |
+
qh = self.to_q(qn).unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 123 |
+
kh = self.to_k(kvn).unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 124 |
+
vh = self.to_v(kvn).unflatten(-1, (self.heads, self.head_dim)).transpose(1, 2)
|
| 125 |
+
if self.use_sdpa:
|
| 126 |
+
out = F.scaled_dot_product_attention(qh, kh, vh, scale=self.head_dim ** -0.5)
|
| 127 |
+
else:
|
| 128 |
+
attn = torch.softmax((qh @ kh.transpose(-2, -1)) * (self.head_dim ** -0.5), dim=-1)
|
| 129 |
+
out = attn @ vh
|
| 130 |
+
x = q + self.out_proj(out.transpose(1, 2).flatten(2))
|
| 131 |
+
x = x + self.mlp(self.norm2(x))
|
| 132 |
+
return x
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class QueryEmbedding(nn.Module):
|
| 136 |
+
def __init__(self, vocab: int = H3_VOCAB, dim: int = 256, out: int = H3_HIDDEN):
|
| 137 |
+
super().__init__()
|
| 138 |
+
self.embed = nn.Embedding(vocab, dim)
|
| 139 |
+
self.proj = nn.Linear(dim, out, bias=True)
|
| 140 |
+
|
| 141 |
+
def forward(self, ids: torch.Tensor) -> torch.Tensor:
|
| 142 |
+
return self.proj(self.embed(ids))
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class H3Adapter(nn.Module):
|
| 146 |
+
"""完整适配器。param 约 1.14B(source_proj 13.8M + query_embed 40.3M + crossattn 319M + refiner 751M)。"""
|
| 147 |
+
|
| 148 |
+
def __init__(self):
|
| 149 |
+
super().__init__()
|
| 150 |
+
self.source_projection = nn.Linear(STUDENT_HIDDEN, H3_HIDDEN, bias=True)
|
| 151 |
+
self.query_embedding = QueryEmbedding()
|
| 152 |
+
self.cross_attention = CrossAttentionBlock()
|
| 153 |
+
self.token_refiner = TokenRefiner()
|
| 154 |
+
|
| 155 |
+
def forward(self, h3_ids: torch.Tensor, student_hidden: torch.Tensor) -> torch.Tensor:
|
| 156 |
+
kv = self.source_projection(student_hidden)
|
| 157 |
+
q = self.query_embedding(h3_ids)
|
| 158 |
+
x = self.cross_attention(q, kv)
|
| 159 |
+
x = self.token_refiner(x)
|
| 160 |
+
return x
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/adapter_clip.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MiniMaxH3AdapterCLIP: 鸭子类型 CLIP,纯 torch(无 ComfyUI 依赖,可独立测试)。
|
| 2 |
+
|
| 3 |
+
contract(与官方 minimax CLIP 对齐):
|
| 4 |
+
tokenize(prompt, images=...) -> 捕获官方节点的 prompt
|
| 5 |
+
encode_from_tokens_scheduled(tokens) -> [[embeds_fp16 [1, S_T, 5376],
|
| 6 |
+
{"minimax_token_tags": tags_long [S_T]}]]
|
| 7 |
+
纯文本 prompt 的 tag 全为 1(DiT packed 布局的 text 段)。
|
| 8 |
+
|
| 9 |
+
原理: ComfyUI MiniMaxH3 DiT 的 preprocess_text_embeds / _forward 在
|
| 10 |
+
text_states.shape[-1] == 5376 (hidden_size) 时跳过 condition_proj + token_refiner,
|
| 11 |
+
适配器输出已是 post-refiner 表示,直接注入。
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
import node_helpers
|
| 19 |
+
except ImportError: # 独立测试环境
|
| 20 |
+
def _conditioning_set_values(cond, new_values):
|
| 21 |
+
if len(cond) == 1 and len(cond[0][1]) == 0:
|
| 22 |
+
return [[cond[0][0], new_values]]
|
| 23 |
+
return [[c[0], {**c[1], **new_values}] for c in cond]
|
| 24 |
+
node_helpers = type("NH", (), {"conditioning_set_values": staticmethod(_conditioning_set_values)})()
|
| 25 |
+
|
| 26 |
+
H3_HIDDEN = 5376
|
| 27 |
+
TEXT_TAG = 1
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class MiniMaxH3AdapterCLIP:
|
| 31 |
+
def __init__(self, adapter: torch.nn.Module, student, h3_tokenizer,
|
| 32 |
+
device: torch.device, output_dtype: torch.dtype = torch.float16,
|
| 33 |
+
lowvram: bool = True):
|
| 34 |
+
self._adapter = adapter
|
| 35 |
+
self._student = student # callable: prompt -> [S_S, 2560] (device)
|
| 36 |
+
self._tok = h3_tokenizer
|
| 37 |
+
self._device = device
|
| 38 |
+
self._dtype = output_dtype
|
| 39 |
+
self._lowvram = lowvram
|
| 40 |
+
self._adapter_offloaded = False
|
| 41 |
+
self._prompt = ""
|
| 42 |
+
|
| 43 |
+
def tokenize(self, text: str, return_word_ids: bool = False, images=None, **kwargs):
|
| 44 |
+
self._prompt = text
|
| 45 |
+
return {}
|
| 46 |
+
|
| 47 |
+
@torch.no_grad()
|
| 48 |
+
def encode_from_tokens_scheduled(self, tokens, unprojected: bool = False,
|
| 49 |
+
add_dict: dict = {}, show_pbar: bool = True):
|
| 50 |
+
prompt = self._prompt
|
| 51 |
+
dev = self._device
|
| 52 |
+
if self._adapter_offloaded:
|
| 53 |
+
self._adapter.to(dev)
|
| 54 |
+
self._adapter_offloaded = False
|
| 55 |
+
ids = self._tok(prompt, add_special_tokens=False)["input_ids"]
|
| 56 |
+
h_s = self._student(prompt).unsqueeze(0).to(dev) # [1, S_S, 2560]
|
| 57 |
+
h3_ids = torch.tensor([ids], dtype=torch.long, device=dev)
|
| 58 |
+
embeds = self._adapter(h3_ids, h_s) # [1, S_T, 5376] bf16
|
| 59 |
+
s_t = embeds.shape[1]
|
| 60 |
+
assert s_t == len(ids), f"h3 ids {len(ids)} != adapter out {s_t}"
|
| 61 |
+
assert embeds.shape[-1] == H3_HIDDEN, embeds.shape
|
| 62 |
+
assert torch.isfinite(embeds).all(), "adapter 输出含 NaN"
|
| 63 |
+
|
| 64 |
+
cond_embeds = embeds.to(self._dtype) # fp16 对齐官方 minimax CLIP
|
| 65 |
+
tags = torch.ones(s_t, dtype=torch.long, device=dev)
|
| 66 |
+
cond = [[cond_embeds, {"minimax_token_tags": tags}]]
|
| 67 |
+
if self._lowvram:
|
| 68 |
+
# 适配器 1.14B(≈2.3GB) 不卸载会挤占 DiT 显存 → lowvram offload,下次 encode 再载入
|
| 69 |
+
self._adapter.to("cpu")
|
| 70 |
+
self._adapter_offloaded = True
|
| 71 |
+
if torch.cuda.is_available():
|
| 72 |
+
torch.cuda.empty_cache()
|
| 73 |
+
if add_dict:
|
| 74 |
+
cond = node_helpers.conditioning_set_values(cond, add_dict)
|
| 75 |
+
return cond
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class MiniMaxH3AdapterFromCLIP:
|
| 79 |
+
"""CLIP→CLIP 包装: 学生 CLIP(CLIPLoader GGUF 等 ComfyUI 原生加载)的 encode 输出当 student_hidden。
|
| 80 |
+
|
| 81 |
+
消费内层 CLIP 契约(comfy.sd.CLIP 或等价鸭子类型):
|
| 82 |
+
tokenize(prompt) -> tokens
|
| 83 |
+
encode_from_tokens(tokens) -> [[tensor [1, S_S, 2560], extras]]
|
| 84 |
+
内层 encode 的 layer="last" 输出 = 学生主干 post-final-norm hidden(与训练 HF 语义一致)。
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def __init__(self, inner_clip, adapter: torch.nn.Module, h3_tokenizer,
|
| 88 |
+
device: torch.device, output_dtype: torch.dtype = torch.float16,
|
| 89 |
+
lowvram: bool = True):
|
| 90 |
+
self._inner = inner_clip
|
| 91 |
+
self._adapter = adapter
|
| 92 |
+
self._tok = h3_tokenizer
|
| 93 |
+
self._device = device
|
| 94 |
+
self._dtype = output_dtype
|
| 95 |
+
self._lowvram = lowvram
|
| 96 |
+
self._adapter_offloaded = False
|
| 97 |
+
self._prompt = ""
|
| 98 |
+
|
| 99 |
+
def tokenize(self, text: str, return_word_ids: bool = False, images=None, **kwargs):
|
| 100 |
+
self._prompt = text
|
| 101 |
+
return {}
|
| 102 |
+
|
| 103 |
+
@torch.no_grad()
|
| 104 |
+
def encode_from_tokens_scheduled(self, tokens, unprojected: bool = False,
|
| 105 |
+
add_dict: dict = {}, show_pbar: bool = True):
|
| 106 |
+
prompt = self._prompt
|
| 107 |
+
if self._adapter_offloaded:
|
| 108 |
+
self._adapter.to(self._device)
|
| 109 |
+
self._adapter_offloaded = False
|
| 110 |
+
inner_tokens = self._inner.tokenize(prompt)
|
| 111 |
+
inner_cond = self._inner.encode_from_tokens(inner_tokens)
|
| 112 |
+
dev = next(self._adapter.parameters()).device # 内层 CLIP 可能在 CPU/低显存模式,以适配器设备为准
|
| 113 |
+
h_s = inner_cond[0][0][0].to(torch.bfloat16).to(dev) # [S_S, 2560] post-norm
|
| 114 |
+
ids = self._tok(prompt, add_special_tokens=False)["input_ids"]
|
| 115 |
+
h3_ids = torch.tensor([ids], dtype=torch.long, device=dev)
|
| 116 |
+
embeds = self._adapter(h3_ids, h_s.unsqueeze(0)) # [1, S_T, 5376] bf16
|
| 117 |
+
s_t = embeds.shape[1]
|
| 118 |
+
assert s_t == len(ids), f"h3 ids {len(ids)} != adapter out {s_t}"
|
| 119 |
+
assert embeds.shape[-1] == H3_HIDDEN, embeds.shape
|
| 120 |
+
assert torch.isfinite(embeds).all(), "adapter 输出含 NaN"
|
| 121 |
+
|
| 122 |
+
cond_embeds = embeds.to(self._dtype)
|
| 123 |
+
tags = torch.ones(s_t, dtype=torch.long, device=dev)
|
| 124 |
+
cond = [[cond_embeds, {"minimax_token_tags": tags}]]
|
| 125 |
+
if self._lowvram:
|
| 126 |
+
self._adapter.to("cpu")
|
| 127 |
+
self._adapter_offloaded = True
|
| 128 |
+
if torch.cuda.is_available():
|
| 129 |
+
torch.cuda.empty_cache()
|
| 130 |
+
if add_dict:
|
| 131 |
+
cond = node_helpers.conditioning_set_values(cond, add_dict)
|
| 132 |
+
return cond
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/check_alignment.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""候选学生(GGUF 量化 / uncensored 微调 / 任意 HF 目录)vs bf16 原版 的特征对齐测试。
|
| 3 |
+
|
| 4 |
+
这是"换学生"的照妖镜——适配器训练时的输入是 bf16 原版的 hidden_states[-1],
|
| 5 |
+
候选版隐藏态的任何漂移都会直接传导到生成的 conditioning。
|
| 6 |
+
|
| 7 |
+
用法:
|
| 8 |
+
python check_alignment.py <bf16 原版目录> <候选: *.gguf 文件 或 HF 目录>
|
| 9 |
+
[--prompts 3] [--device cuda]
|
| 10 |
+
|
| 11 |
+
判定:
|
| 12 |
+
cos > 0.99 合格(量化级噪声,直接用)
|
| 13 |
+
0.95-0.99 有可见漂移,评估后可用(建议先跑一次端到端生成对比)
|
| 14 |
+
< 0.95 漂移显著,需用候选学生重提特征 + 重训/微调适配器
|
| 15 |
+
|
| 16 |
+
说明:
|
| 17 |
+
GGUF 走 transformers 内置加载器(>=4.45,AutoModel.from_pretrained(".gguf")),
|
| 18 |
+
隐藏态语义与 bf16 原版完全一致(都是 transformers 路径),测的是量化+微调漂移本身。
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import argparse
|
| 23 |
+
import os
|
| 24 |
+
import sys
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
|
| 28 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 29 |
+
sys.path.insert(0, HERE)
|
| 30 |
+
|
| 31 |
+
from student import StudentTextEncoder, load_student, find_language_model # noqa: E402
|
| 32 |
+
from adapter_clip import H3_HIDDEN # noqa: E402
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_candidate(path: str, device: torch.device, baseline_mem: float = 0.0):
|
| 36 |
+
from transformers import AutoConfig, AutoModel
|
| 37 |
+
|
| 38 |
+
if path.endswith(".gguf"):
|
| 39 |
+
from gguf_qwen35 import load_gguf_model_quantized
|
| 40 |
+
model, info = load_gguf_model_quantized(path)
|
| 41 |
+
print(f" [candidate] 自定义 GGUF 加载器(量化驻留): {info}", flush=True)
|
| 42 |
+
model = model.to(torch.bfloat16).to(device) # GGMLTensor 的 to 忽略 dtype, 只移设备
|
| 43 |
+
if device.type == "cuda":
|
| 44 |
+
used = torch.cuda.memory_allocated() / 2**30 - baseline_mem
|
| 45 |
+
print(f" [candidate] 净显存: {used:.2f} GiB", flush=True)
|
| 46 |
+
return model
|
| 47 |
+
# HF 目录
|
| 48 |
+
cfg = AutoConfig.from_pretrained(path)
|
| 49 |
+
model = AutoModel.from_config(cfg)
|
| 50 |
+
try:
|
| 51 |
+
sd = {k[len("model."):] if k.startswith("model.") else k: v
|
| 52 |
+
for k, v in torch.load(os.path.join(path, "pytorch_model.bin"), map_location="cpu").items()}
|
| 53 |
+
missing, _ = model.load_state_dict(sd, strict=False)
|
| 54 |
+
assert not missing, f"missing: {missing[:5]}"
|
| 55 |
+
except FileNotFoundError:
|
| 56 |
+
model = AutoModel.from_pretrained(path, torch_dtype=torch.bfloat16)
|
| 57 |
+
return model.to(device)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@torch.no_grad()
|
| 61 |
+
def hidden_fn(lm, tok, prompt: str, device: torch.device) -> torch.Tensor:
|
| 62 |
+
ids = tok(prompt, add_special_tokens=False)["input_ids"]
|
| 63 |
+
out = lm(input_ids=torch.tensor([ids], device=device), output_hidden_states=True)
|
| 64 |
+
return out.hidden_states[-1][0].float() # [S, D] 与训练同路径
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def main() -> None:
|
| 68 |
+
ap = argparse.ArgumentParser()
|
| 69 |
+
ap.add_argument("reference", help="bf16 原版 Qwen3.5-4B HF 目录")
|
| 70 |
+
ap.add_argument("candidate", help="候选: .gguf 或 HF 目录")
|
| 71 |
+
ap.add_argument("--prompts", type=int, default=3)
|
| 72 |
+
ap.add_argument("--device", default="")
|
| 73 |
+
args = ap.parse_args()
|
| 74 |
+
|
| 75 |
+
from transformers import AutoTokenizer
|
| 76 |
+
dev = torch.device(args.device) if args.device else torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 77 |
+
print(f"device: {dev}")
|
| 78 |
+
|
| 79 |
+
print("[1/2] loading reference (bf16)...", flush=True)
|
| 80 |
+
ref_model = load_student(args.reference).to(torch.bfloat16).to(dev)
|
| 81 |
+
ref_lm = find_language_model(ref_model)
|
| 82 |
+
ref_tok = AutoTokenizer.from_pretrained(args.reference)
|
| 83 |
+
|
| 84 |
+
print("[2/2] loading candidate...", flush=True)
|
| 85 |
+
baseline = torch.cuda.memory_allocated() / 2**30 if dev.type == "cuda" else 0.0
|
| 86 |
+
cand_model = load_candidate(args.candidate, dev, baseline)
|
| 87 |
+
cand_lm = find_language_model(cand_model)
|
| 88 |
+
# .gguf 候选无自带 tokenizer: 复用参考模型的(同一 Qwen3.5-4B 词表)
|
| 89 |
+
cand_tok = ref_tok if args.candidate.endswith(".gguf") else AutoTokenizer.from_pretrained(args.candidate)
|
| 90 |
+
|
| 91 |
+
demo = [
|
| 92 |
+
"A cinematic shot of a tiny robot repairing a broken music box in a giant's attic",
|
| 93 |
+
"A surfer riding a massive neon wave at sunset, spray frozen in the air, dramatic backlight",
|
| 94 |
+
"Paper collage birds flocking over a misty city skyline, soft morning light",
|
| 95 |
+
]
|
| 96 |
+
print(f"\ncosine 对比 {args.prompts} 条 prompt(hidden_states[-1], [S, 2560]):")
|
| 97 |
+
worst = 1.0
|
| 98 |
+
for i, p in enumerate(demo[: args.prompts]):
|
| 99 |
+
h_ref = hidden_fn(ref_lm, ref_tok, p, dev)
|
| 100 |
+
h_can = hidden_fn(cand_lm, cand_tok, p, dev)
|
| 101 |
+
n = min(h_ref.shape[0], h_can.shape[0])
|
| 102 |
+
if h_ref.shape[0] != h_can.shape[0]:
|
| 103 |
+
print(f" [注意] prompt {i}: 长度不同 ref={h_ref.shape[0]} cand={h_can.shape[0]}(截断到 {n} 比较)")
|
| 104 |
+
a, b = h_ref[:n].flatten(), h_can[:n].flatten()
|
| 105 |
+
cos = (a * b).sum() / (a.norm() * b.norm())
|
| 106 |
+
worst = min(worst, cos.item())
|
| 107 |
+
print(f" prompt {i}: cos={cos.item():.4f}")
|
| 108 |
+
|
| 109 |
+
print(f"\n最差 cos = {worst:.4f} -> ", end="")
|
| 110 |
+
if worst > 0.99:
|
| 111 |
+
print("合格(量化级噪声,直接用)✅")
|
| 112 |
+
elif worst >= 0.95:
|
| 113 |
+
print("有可见漂移,评估后可用(建议先端到端生成对比)⚠️")
|
| 114 |
+
else:
|
| 115 |
+
print("漂移显著,需用候选学生重提特征 + 重训/微调适配器 ❌")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
main()
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/export_adapter.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""导出 train.py checkpoint -> ComfyUI 适配器 .safetensors(纯 state_dict)。
|
| 3 |
+
|
| 4 |
+
用法:
|
| 5 |
+
python export_adapter.py <adapter_stage2_step2000.pt> [--out adapter_stage2.safetensors]
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 16 |
+
sys.path.insert(0, HERE)
|
| 17 |
+
|
| 18 |
+
from adapter.model import H3Adapter # noqa: E402
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main() -> None:
|
| 22 |
+
ap = argparse.ArgumentParser()
|
| 23 |
+
ap.add_argument("ckpt", help="train.py checkpoint (.pt)")
|
| 24 |
+
ap.add_argument("--out", default="adapter_stage2.safetensors")
|
| 25 |
+
args = ap.parse_args()
|
| 26 |
+
|
| 27 |
+
ckpt = torch.load(args.ckpt, map_location="cpu", weights_only=False)
|
| 28 |
+
state = ckpt.get("model", ckpt) # 兼容 {"model":..., "config":...} 与纯 state_dict
|
| 29 |
+
if any(k.startswith("model.") for k in state):
|
| 30 |
+
state = {k[len("model."):]: v for k, v in state.items()}
|
| 31 |
+
|
| 32 |
+
model = H3Adapter()
|
| 33 |
+
missing, unexpected = model.load_state_dict(state, strict=False)
|
| 34 |
+
assert not missing, f"missing: {missing[:10]}"
|
| 35 |
+
print(f"adapter keys: {len(state)} | missing={len(missing)} unexpected={len(unexpected)}")
|
| 36 |
+
|
| 37 |
+
from safetensors.torch import save_file
|
| 38 |
+
save_file({k: v.contiguous().to(torch.bfloat16) for k, v in state.items()}, args.out)
|
| 39 |
+
print(f"saved -> {args.out}")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
main()
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/export_cond.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""临时调试节点: 导出 CLIP 的 cond embeds 到 models/minimax_h3_adapter/cond_official_ref.npy。
|
| 2 |
+
|
| 3 |
+
用法(黑屏诊断,插进官方工作流):
|
| 4 |
+
CLIPLoader(官方 qwen3vl-32B GGUF) -> MiniMaxH3ExportCond -> MiniMaxH3ImageToVideo
|
| 5 |
+
节点透传 clip 对象(不改变工作流行为),执行时导出 cond npy 并打印路径。
|
| 6 |
+
ComfyUI 惰性执行会剪枝无输出节点,故必须输出透传 clip 才能被触发。
|
| 7 |
+
"""
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
import folder_paths
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class MiniMaxH3ExportCond:
|
| 16 |
+
@classmethod
|
| 17 |
+
def INPUT_TYPES(cls):
|
| 18 |
+
return {"required": {
|
| 19 |
+
"clip": ("CLIP",),
|
| 20 |
+
"prompt": ("STRING", {"default": "a red apple rotating on a wooden table, studio lighting, 3d render",
|
| 21 |
+
"multiline": True}),
|
| 22 |
+
}}
|
| 23 |
+
|
| 24 |
+
RETURN_TYPES = ("CLIP",)
|
| 25 |
+
RETURN_NAMES = ("clip",)
|
| 26 |
+
FUNCTION = "run"
|
| 27 |
+
CATEGORY = "model/conditioning/minimax"
|
| 28 |
+
|
| 29 |
+
def run(self, clip, prompt):
|
| 30 |
+
tokens = clip.tokenize(prompt)
|
| 31 |
+
cond = clip.encode_from_tokens_scheduled(tokens)
|
| 32 |
+
embeds = cond[0][0].float().cpu().numpy()
|
| 33 |
+
out_dir = folder_paths.get_folder_paths("minimax_h3_adapter")[0]
|
| 34 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 35 |
+
path = os.path.join(out_dir, "cond_official_ref.npy")
|
| 36 |
+
np.save(path, embeds)
|
| 37 |
+
extra = ""
|
| 38 |
+
if len(cond[0]) > 1 and isinstance(cond[0][1], dict) and "minimax_token_tags" in cond[0][1]:
|
| 39 |
+
tags = cond[0][1]["minimax_token_tags"].cpu().numpy()
|
| 40 |
+
extra = f" tags_unique={sorted(set(tags.tolist()))}"
|
| 41 |
+
print(f"[export_cond] saved {path} shape={embeds.shape} dtype={embeds.dtype}{extra}", flush=True)
|
| 42 |
+
return (clip,)
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/gguf_dequant.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
|
| 2 |
+
import gguf
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
except ImportError:
|
| 8 |
+
class _Tqdm:
|
| 9 |
+
@staticmethod
|
| 10 |
+
def write(msg):
|
| 11 |
+
print(msg)
|
| 12 |
+
tqdm = _Tqdm
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
TORCH_COMPATIBLE_QTYPES = (None, gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16)
|
| 16 |
+
|
| 17 |
+
def is_torch_compatible(tensor):
|
| 18 |
+
return tensor is None or getattr(tensor, "tensor_type", None) in TORCH_COMPATIBLE_QTYPES
|
| 19 |
+
|
| 20 |
+
def is_quantized(tensor):
|
| 21 |
+
return not is_torch_compatible(tensor)
|
| 22 |
+
|
| 23 |
+
def dequantize_tensor(tensor, dtype=None, dequant_dtype=None):
|
| 24 |
+
qtype = getattr(tensor, "tensor_type", None)
|
| 25 |
+
oshape = getattr(tensor, "tensor_shape", tensor.shape)
|
| 26 |
+
|
| 27 |
+
if qtype in TORCH_COMPATIBLE_QTYPES:
|
| 28 |
+
return tensor.to(dtype)
|
| 29 |
+
elif qtype in dequantize_functions:
|
| 30 |
+
dequant_dtype = dtype if dequant_dtype == "target" else dequant_dtype
|
| 31 |
+
return dequantize(tensor.data, qtype, oshape, dtype=dequant_dtype).to(dtype)
|
| 32 |
+
else:
|
| 33 |
+
# this is incredibly slow
|
| 34 |
+
tqdm.write(f"Falling back to numpy dequant for qtype: {getattr(qtype, 'name', repr(qtype))}")
|
| 35 |
+
new = gguf.quants.dequantize(tensor.cpu().numpy(), qtype)
|
| 36 |
+
return torch.from_numpy(new).to(tensor.device, dtype=dtype)
|
| 37 |
+
|
| 38 |
+
def dequantize(data, qtype, oshape, dtype=None):
|
| 39 |
+
"""
|
| 40 |
+
Dequantize tensor back to usable shape/dtype
|
| 41 |
+
"""
|
| 42 |
+
block_size, type_size = gguf.GGML_QUANT_SIZES[qtype]
|
| 43 |
+
dequantize_blocks = dequantize_functions[qtype]
|
| 44 |
+
|
| 45 |
+
rows = data.reshape(
|
| 46 |
+
(-1, data.shape[-1])
|
| 47 |
+
).view(torch.uint8)
|
| 48 |
+
|
| 49 |
+
n_blocks = rows.numel() // type_size
|
| 50 |
+
blocks = rows.reshape((n_blocks, type_size))
|
| 51 |
+
blocks = dequantize_blocks(blocks, block_size, type_size, dtype)
|
| 52 |
+
return blocks.reshape(oshape)
|
| 53 |
+
|
| 54 |
+
def to_uint32(x):
|
| 55 |
+
# no uint32 :(
|
| 56 |
+
x = x.view(torch.uint8).to(torch.int32)
|
| 57 |
+
return (x[:, 0] | x[:, 1] << 8 | x[:, 2] << 16 | x[:, 3] << 24).unsqueeze(1)
|
| 58 |
+
|
| 59 |
+
def to_uint16(x):
|
| 60 |
+
x = x.view(torch.uint8).to(torch.int32)
|
| 61 |
+
return (x[:, 0] | x[:, 1] << 8).unsqueeze(1)
|
| 62 |
+
|
| 63 |
+
def split_block_dims(blocks, *args):
|
| 64 |
+
n_max = blocks.shape[1]
|
| 65 |
+
dims = list(args) + [n_max - sum(args)]
|
| 66 |
+
return torch.split(blocks, dims, dim=1)
|
| 67 |
+
|
| 68 |
+
# Full weights #
|
| 69 |
+
def dequantize_blocks_BF16(blocks, block_size, type_size, dtype=None):
|
| 70 |
+
return (blocks.view(torch.int16).to(torch.int32) << 16).view(torch.float32)
|
| 71 |
+
|
| 72 |
+
# Legacy Quants #
|
| 73 |
+
def dequantize_blocks_Q8_0(blocks, block_size, type_size, dtype=None):
|
| 74 |
+
d, x = split_block_dims(blocks, 2)
|
| 75 |
+
d = d.view(torch.float16).to(dtype)
|
| 76 |
+
x = x.view(torch.int8)
|
| 77 |
+
return (d * x)
|
| 78 |
+
|
| 79 |
+
def dequantize_blocks_Q5_1(blocks, block_size, type_size, dtype=None):
|
| 80 |
+
n_blocks = blocks.shape[0]
|
| 81 |
+
|
| 82 |
+
d, m, qh, qs = split_block_dims(blocks, 2, 2, 4)
|
| 83 |
+
d = d.view(torch.float16).to(dtype)
|
| 84 |
+
m = m.view(torch.float16).to(dtype)
|
| 85 |
+
qh = to_uint32(qh)
|
| 86 |
+
|
| 87 |
+
qh = qh.reshape((n_blocks, 1)) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32)
|
| 88 |
+
ql = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(1, 1, 2, 1)
|
| 89 |
+
qh = (qh & 1).to(torch.uint8)
|
| 90 |
+
ql = (ql & 0x0F).reshape((n_blocks, -1))
|
| 91 |
+
|
| 92 |
+
qs = (ql | (qh << 4))
|
| 93 |
+
return (d * qs) + m
|
| 94 |
+
|
| 95 |
+
def dequantize_blocks_Q5_0(blocks, block_size, type_size, dtype=None):
|
| 96 |
+
n_blocks = blocks.shape[0]
|
| 97 |
+
|
| 98 |
+
d, qh, qs = split_block_dims(blocks, 2, 4)
|
| 99 |
+
d = d.view(torch.float16).to(dtype)
|
| 100 |
+
qh = to_uint32(qh)
|
| 101 |
+
|
| 102 |
+
qh = qh.reshape(n_blocks, 1) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32)
|
| 103 |
+
ql = qs.reshape(n_blocks, -1, 1, block_size // 2) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(1, 1, 2, 1)
|
| 104 |
+
|
| 105 |
+
qh = (qh & 1).to(torch.uint8)
|
| 106 |
+
ql = (ql & 0x0F).reshape(n_blocks, -1)
|
| 107 |
+
|
| 108 |
+
qs = (ql | (qh << 4)).to(torch.int8) - 16
|
| 109 |
+
return (d * qs)
|
| 110 |
+
|
| 111 |
+
def dequantize_blocks_Q4_1(blocks, block_size, type_size, dtype=None):
|
| 112 |
+
n_blocks = blocks.shape[0]
|
| 113 |
+
|
| 114 |
+
d, m, qs = split_block_dims(blocks, 2, 2)
|
| 115 |
+
d = d.view(torch.float16).to(dtype)
|
| 116 |
+
m = m.view(torch.float16).to(dtype)
|
| 117 |
+
|
| 118 |
+
qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(1, 1, 2, 1)
|
| 119 |
+
qs = (qs & 0x0F).reshape(n_blocks, -1)
|
| 120 |
+
|
| 121 |
+
return (d * qs) + m
|
| 122 |
+
|
| 123 |
+
def dequantize_blocks_Q4_0(blocks, block_size, type_size, dtype=None):
|
| 124 |
+
n_blocks = blocks.shape[0]
|
| 125 |
+
|
| 126 |
+
d, qs = split_block_dims(blocks, 2)
|
| 127 |
+
d = d.view(torch.float16).to(dtype)
|
| 128 |
+
|
| 129 |
+
qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1))
|
| 130 |
+
qs = (qs & 0x0F).reshape((n_blocks, -1)).to(torch.int8) - 8
|
| 131 |
+
return (d * qs)
|
| 132 |
+
|
| 133 |
+
# K Quants #
|
| 134 |
+
QK_K = 256
|
| 135 |
+
K_SCALE_SIZE = 12
|
| 136 |
+
|
| 137 |
+
def get_scale_min(scales):
|
| 138 |
+
n_blocks = scales.shape[0]
|
| 139 |
+
scales = scales.view(torch.uint8)
|
| 140 |
+
scales = scales.reshape((n_blocks, 3, 4))
|
| 141 |
+
|
| 142 |
+
d, m, m_d = torch.split(scales, scales.shape[-2] // 3, dim=-2)
|
| 143 |
+
|
| 144 |
+
sc = torch.cat([d & 0x3F, (m_d & 0x0F) | ((d >> 2) & 0x30)], dim=-1)
|
| 145 |
+
min = torch.cat([m & 0x3F, (m_d >> 4) | ((m >> 2) & 0x30)], dim=-1)
|
| 146 |
+
|
| 147 |
+
return (sc.reshape((n_blocks, 8)), min.reshape((n_blocks, 8)))
|
| 148 |
+
|
| 149 |
+
def dequantize_blocks_Q6_K(blocks, block_size, type_size, dtype=None):
|
| 150 |
+
n_blocks = blocks.shape[0]
|
| 151 |
+
|
| 152 |
+
ql, qh, scales, d, = split_block_dims(blocks, QK_K // 2, QK_K // 4, QK_K // 16)
|
| 153 |
+
|
| 154 |
+
scales = scales.view(torch.int8).to(dtype)
|
| 155 |
+
d = d.view(torch.float16).to(dtype)
|
| 156 |
+
d = (d * scales).reshape((n_blocks, QK_K // 16, 1))
|
| 157 |
+
|
| 158 |
+
ql = ql.reshape((n_blocks, -1, 1, 64)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1))
|
| 159 |
+
ql = (ql & 0x0F).reshape((n_blocks, -1, 32))
|
| 160 |
+
qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1))
|
| 161 |
+
qh = (qh & 0x03).reshape((n_blocks, -1, 32))
|
| 162 |
+
q = (ql | (qh << 4)).to(torch.int8) - 32
|
| 163 |
+
q = q.reshape((n_blocks, QK_K // 16, -1))
|
| 164 |
+
|
| 165 |
+
return (d * q).reshape((n_blocks, QK_K))
|
| 166 |
+
|
| 167 |
+
def dequantize_blocks_Q5_K(blocks, block_size, type_size, dtype=None):
|
| 168 |
+
n_blocks = blocks.shape[0]
|
| 169 |
+
|
| 170 |
+
d, dmin, scales, qh, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE, QK_K // 8)
|
| 171 |
+
|
| 172 |
+
d = d.view(torch.float16).to(dtype)
|
| 173 |
+
dmin = dmin.view(torch.float16).to(dtype)
|
| 174 |
+
|
| 175 |
+
sc, m = get_scale_min(scales)
|
| 176 |
+
|
| 177 |
+
d = (d * sc).reshape((n_blocks, -1, 1))
|
| 178 |
+
dm = (dmin * m).reshape((n_blocks, -1, 1))
|
| 179 |
+
|
| 180 |
+
ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1))
|
| 181 |
+
qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([i for i in range(8)], device=d.device, dtype=torch.uint8).reshape((1, 1, 8, 1))
|
| 182 |
+
ql = (ql & 0x0F).reshape((n_blocks, -1, 32))
|
| 183 |
+
qh = (qh & 0x01).reshape((n_blocks, -1, 32))
|
| 184 |
+
q = (ql | (qh << 4))
|
| 185 |
+
|
| 186 |
+
return (d * q - dm).reshape((n_blocks, QK_K))
|
| 187 |
+
|
| 188 |
+
def dequantize_blocks_Q4_K(blocks, block_size, type_size, dtype=None):
|
| 189 |
+
n_blocks = blocks.shape[0]
|
| 190 |
+
|
| 191 |
+
d, dmin, scales, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE)
|
| 192 |
+
d = d.view(torch.float16).to(dtype)
|
| 193 |
+
dmin = dmin.view(torch.float16).to(dtype)
|
| 194 |
+
|
| 195 |
+
sc, m = get_scale_min(scales)
|
| 196 |
+
|
| 197 |
+
d = (d * sc).reshape((n_blocks, -1, 1))
|
| 198 |
+
dm = (dmin * m).reshape((n_blocks, -1, 1))
|
| 199 |
+
|
| 200 |
+
qs = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1))
|
| 201 |
+
qs = (qs & 0x0F).reshape((n_blocks, -1, 32))
|
| 202 |
+
|
| 203 |
+
return (d * qs - dm).reshape((n_blocks, QK_K))
|
| 204 |
+
|
| 205 |
+
def dequantize_blocks_Q3_K(blocks, block_size, type_size, dtype=None):
|
| 206 |
+
n_blocks = blocks.shape[0]
|
| 207 |
+
|
| 208 |
+
hmask, qs, scales, d = split_block_dims(blocks, QK_K // 8, QK_K // 4, 12)
|
| 209 |
+
d = d.view(torch.float16).to(dtype)
|
| 210 |
+
|
| 211 |
+
lscales, hscales = scales[:, :8], scales[:, 8:]
|
| 212 |
+
lscales = lscales.reshape((n_blocks, 1, 8)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 2, 1))
|
| 213 |
+
lscales = lscales.reshape((n_blocks, 16))
|
| 214 |
+
hscales = hscales.reshape((n_blocks, 1, 4)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 4, 1))
|
| 215 |
+
hscales = hscales.reshape((n_blocks, 16))
|
| 216 |
+
scales = (lscales & 0x0F) | ((hscales & 0x03) << 4)
|
| 217 |
+
scales = (scales.to(torch.int8) - 32)
|
| 218 |
+
|
| 219 |
+
dl = (d * scales).reshape((n_blocks, 16, 1))
|
| 220 |
+
|
| 221 |
+
ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1))
|
| 222 |
+
qh = hmask.reshape(n_blocks, -1, 1, 32) >> torch.tensor([i for i in range(8)], device=d.device, dtype=torch.uint8).reshape((1, 1, 8, 1))
|
| 223 |
+
ql = ql.reshape((n_blocks, 16, QK_K // 16)) & 3
|
| 224 |
+
qh = (qh.reshape((n_blocks, 16, QK_K // 16)) & 1) ^ 1
|
| 225 |
+
q = (ql.to(torch.int8) - (qh << 2).to(torch.int8))
|
| 226 |
+
|
| 227 |
+
return (dl * q).reshape((n_blocks, QK_K))
|
| 228 |
+
|
| 229 |
+
def dequantize_blocks_Q2_K(blocks, block_size, type_size, dtype=None):
|
| 230 |
+
n_blocks = blocks.shape[0]
|
| 231 |
+
|
| 232 |
+
scales, qs, d, dmin = split_block_dims(blocks, QK_K // 16, QK_K // 4, 2)
|
| 233 |
+
d = d.view(torch.float16).to(dtype)
|
| 234 |
+
dmin = dmin.view(torch.float16).to(dtype)
|
| 235 |
+
|
| 236 |
+
# (n_blocks, 16, 1)
|
| 237 |
+
dl = (d * (scales & 0xF)).reshape((n_blocks, QK_K // 16, 1))
|
| 238 |
+
ml = (dmin * (scales >> 4)).reshape((n_blocks, QK_K // 16, 1))
|
| 239 |
+
|
| 240 |
+
shift = torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1))
|
| 241 |
+
|
| 242 |
+
qs = (qs.reshape((n_blocks, -1, 1, 32)) >> shift) & 3
|
| 243 |
+
qs = qs.reshape((n_blocks, QK_K // 16, 16))
|
| 244 |
+
qs = dl * qs - ml
|
| 245 |
+
|
| 246 |
+
return qs.reshape((n_blocks, -1))
|
| 247 |
+
|
| 248 |
+
# IQ quants
|
| 249 |
+
KVALUES = torch.tensor([-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113], dtype=torch.int8)
|
| 250 |
+
|
| 251 |
+
def dequantize_blocks_IQ4_NL(blocks, block_size, type_size, dtype=None):
|
| 252 |
+
n_blocks = blocks.shape[0]
|
| 253 |
+
|
| 254 |
+
d, qs = split_block_dims(blocks, 2)
|
| 255 |
+
d = d.view(torch.float16).to(dtype)
|
| 256 |
+
|
| 257 |
+
qs = qs.reshape((n_blocks, -1, 1, block_size//2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1))
|
| 258 |
+
qs = (qs & 0x0F).reshape((n_blocks, -1, 1)).to(torch.int64)
|
| 259 |
+
|
| 260 |
+
kvalues = KVALUES.to(qs.device).expand(*qs.shape[:-1], 16)
|
| 261 |
+
qs = torch.gather(kvalues, dim=-1, index=qs).reshape((n_blocks, -1))
|
| 262 |
+
del kvalues # should still be view, but just to be safe
|
| 263 |
+
|
| 264 |
+
return (d * qs)
|
| 265 |
+
|
| 266 |
+
def dequantize_blocks_IQ4_XS(blocks, block_size, type_size, dtype=None):
|
| 267 |
+
n_blocks = blocks.shape[0]
|
| 268 |
+
d, scales_h, scales_l, qs = split_block_dims(blocks, 2, 2, QK_K // 64)
|
| 269 |
+
d = d.view(torch.float16).to(dtype)
|
| 270 |
+
scales_h = to_uint16(scales_h)
|
| 271 |
+
|
| 272 |
+
shift_a = torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2))
|
| 273 |
+
shift_b = torch.tensor([2 * i for i in range(QK_K // 32)], device=d.device, dtype=torch.uint8).reshape((1, -1, 1))
|
| 274 |
+
|
| 275 |
+
scales_l = scales_l.reshape((n_blocks, -1, 1)) >> shift_a.reshape((1, 1, 2))
|
| 276 |
+
scales_h = scales_h.reshape((n_blocks, -1, 1)) >> shift_b.reshape((1, -1, 1))
|
| 277 |
+
|
| 278 |
+
scales_l = scales_l.reshape((n_blocks, -1)) & 0x0F
|
| 279 |
+
scales_h = scales_h.reshape((n_blocks, -1)).to(torch.uint8) & 0x03
|
| 280 |
+
|
| 281 |
+
scales = (scales_l | (scales_h << 4)).to(torch.int8) - 32
|
| 282 |
+
dl = (d * scales.to(dtype)).reshape((n_blocks, -1, 1))
|
| 283 |
+
|
| 284 |
+
qs = qs.reshape((n_blocks, -1, 1, 16)) >> shift_a.reshape((1, 1, 2, 1))
|
| 285 |
+
qs = qs.reshape((n_blocks, -1, 32, 1)) & 0x0F
|
| 286 |
+
|
| 287 |
+
kvalues = KVALUES.to(qs.device).expand(*qs.shape[:-1], 16)
|
| 288 |
+
qs = torch.gather(kvalues, dim=-1, index=qs.to(torch.int64)).reshape((n_blocks, -1, 32))
|
| 289 |
+
del kvalues # see IQ4_NL
|
| 290 |
+
del shift_a
|
| 291 |
+
del shift_b
|
| 292 |
+
|
| 293 |
+
return (dl * qs).reshape((n_blocks, -1))
|
| 294 |
+
|
| 295 |
+
dequantize_functions = {
|
| 296 |
+
gguf.GGMLQuantizationType.BF16: dequantize_blocks_BF16,
|
| 297 |
+
gguf.GGMLQuantizationType.Q8_0: dequantize_blocks_Q8_0,
|
| 298 |
+
gguf.GGMLQuantizationType.Q5_1: dequantize_blocks_Q5_1,
|
| 299 |
+
gguf.GGMLQuantizationType.Q5_0: dequantize_blocks_Q5_0,
|
| 300 |
+
gguf.GGMLQuantizationType.Q4_1: dequantize_blocks_Q4_1,
|
| 301 |
+
gguf.GGMLQuantizationType.Q4_0: dequantize_blocks_Q4_0,
|
| 302 |
+
gguf.GGMLQuantizationType.Q6_K: dequantize_blocks_Q6_K,
|
| 303 |
+
gguf.GGMLQuantizationType.Q5_K: dequantize_blocks_Q5_K,
|
| 304 |
+
gguf.GGMLQuantizationType.Q4_K: dequantize_blocks_Q4_K,
|
| 305 |
+
gguf.GGMLQuantizationType.Q3_K: dequantize_blocks_Q3_K,
|
| 306 |
+
gguf.GGMLQuantizationType.Q2_K: dequantize_blocks_Q2_K,
|
| 307 |
+
gguf.GGMLQuantizationType.IQ4_NL: dequantize_blocks_IQ4_NL,
|
| 308 |
+
gguf.GGMLQuantizationType.IQ4_XS: dequantize_blocks_IQ4_XS,
|
| 309 |
+
}
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/gguf_qwen35.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""自定义 Qwen3.5-4B GGUF 加载器(llama.cpp qwen35 arch -> HF state dict)。
|
| 2 |
+
|
| 3 |
+
动机: city96/ComfyUI-GGUF 与旧版 transformers 均不支持 qwen35 arch 的 GGUF,
|
| 4 |
+
本项目自研读取 + 反量化 + 张量名映射,产出与训练 checkpoint 同格式的
|
| 5 |
+
state dict(language_model.*,与 load_student 剥离前缀后一致)。
|
| 6 |
+
|
| 7 |
+
本加载器的全部布局假设均经"逐张量 cos 对比真实 HauhauCS GGUF vs bf16 参考"实证
|
| 8 |
+
(426/426 张量 cos>=0.99,见 diag 记录):
|
| 9 |
+
|
| 10 |
+
1. 2D 张量数据即 HF [out, in] 行序(元数据 shape 声称 [in, out],二者相反)。
|
| 11 |
+
反量化 flat 序已是 [out, in],reshape 成 (shape[1], shape[0]) 即可,禁止转置。
|
| 12 |
+
2. 范数权重(input/post_attention/q_norm/k_norm/output_norm)GGUF 存真值 w,
|
| 13 |
+
而 HF checkpoint 存增量 (w-1)(Qwen3_5RMSNorm 前向做 * (1 + weight)),
|
| 14 |
+
加载时统一减 1.0。ssm_norm(Qwen3_5RMSNormGated)无增量约定,不减。
|
| 15 |
+
3. linear_attn 的 32 个 v-head 参数(A_log/dt_bias/in_proj_a/b 行、
|
| 16 |
+
in_proj_z 及 qkv/conv1d 的 value 段)GGUF 按 value-major 枚举
|
| 17 |
+
(head = v*16 + k),HF 按 key-major(head = k*2 + v),需逆置换。
|
| 18 |
+
4. ssm_a 存 -A(负衰减),HF 需 A_log = log(A),即 log(-x) 再置换。
|
| 19 |
+
|
| 20 |
+
head 配置 (qwen35_4b): 全注意力 head_dim=256; 线性层 key=16x128 value=32x128; vocab=248320
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import os
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import torch.nn as nn
|
| 28 |
+
import torch.nn.functional as F
|
| 29 |
+
|
| 30 |
+
try: # ComfyUI 包内加载(节点目录不在 sys.path,必须相对导入)
|
| 31 |
+
from .gguf_dequant import dequantize_tensor
|
| 32 |
+
except ImportError: # 独立脚本/顶层加载
|
| 33 |
+
from gguf_dequant import dequantize_tensor
|
| 34 |
+
|
| 35 |
+
FULL_ATTN_LAYERS = {i for i in range(32) if (i + 1) % 4 == 0} # 3,7,11,...,31
|
| 36 |
+
V_HEADS, V_PER_KEY = 32, 2 # value head 数 = 32, 每个 key head 配 2 个 value head
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class GGMLTensor(torch.Tensor):
|
| 40 |
+
"""量化块驻留张量(ComfyUI-GGUF 同款): 底层 uint8 量化字节 + 逻辑形状元数据。
|
| 41 |
+
|
| 42 |
+
前向时由 dequantize_tensor 逐层反量化,避免整模型展开的显存开销。
|
| 43 |
+
属性必须在 __new__ 设置(torch 子类实例化常跳过 __init__)。
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __new__(cls, data=None, tensor_type=None, tensor_shape=None):
|
| 47 |
+
obj = super().__new__(cls, data) if data is not None else super().__new__(cls)
|
| 48 |
+
if tensor_type is not None:
|
| 49 |
+
obj.tensor_type = tensor_type
|
| 50 |
+
obj.tensor_shape = tensor_shape
|
| 51 |
+
return obj
|
| 52 |
+
|
| 53 |
+
def to(self, *args, **kwargs):
|
| 54 |
+
kwargs.pop("dtype", None) # 底层是量化字节,不可 cast dtype,只迁移设备
|
| 55 |
+
if args and isinstance(args[0], torch.dtype):
|
| 56 |
+
args = args[1:]
|
| 57 |
+
new = super().to(*args, **kwargs)
|
| 58 |
+
ts = getattr(self, "tensor_shape", None)
|
| 59 |
+
new.tensor_type = getattr(self, "tensor_type", None)
|
| 60 |
+
new.tensor_shape = ts if ts is not None else tuple(new.data.size())
|
| 61 |
+
return new
|
| 62 |
+
|
| 63 |
+
def clone(self, *args, **kwargs):
|
| 64 |
+
return self
|
| 65 |
+
|
| 66 |
+
def detach(self, *args, **kwargs):
|
| 67 |
+
return self
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def shape(self):
|
| 71 |
+
return self.tensor_shape
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class QuantizedLinear(nn.Module):
|
| 75 |
+
"""前向时反量化权重的 Linear(ComfyUI-GGUF GGMLOps.Linear 同款,纯 torch)。
|
| 76 |
+
|
| 77 |
+
weight 为普通属性而非 Parameter: 避免 Module.to(dtype)/Parameter 包装破坏
|
| 78 |
+
GGMLTensor 的量化元数据;设备在 forward 懒迁移。
|
| 79 |
+
weight_perm: 反量化后的一次性行/列置换(处理 GGUF 的 value-major 头序)。
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
def __init__(self, in_features, out_features, bias=True,
|
| 83 |
+
weight_perm=None, weight_perm_dim=None):
|
| 84 |
+
super().__init__()
|
| 85 |
+
self.in_features = in_features
|
| 86 |
+
self.out_features = out_features
|
| 87 |
+
self.weight = None
|
| 88 |
+
self.bias = None
|
| 89 |
+
self.weight_perm = weight_perm
|
| 90 |
+
self.weight_perm_dim = weight_perm_dim
|
| 91 |
+
|
| 92 |
+
def forward(self, x):
|
| 93 |
+
w = self.weight
|
| 94 |
+
if w is None:
|
| 95 |
+
w = torch.zeros(self.in_features, self.out_features, device=x.device, dtype=x.dtype)
|
| 96 |
+
if w.device != x.device:
|
| 97 |
+
w = self.weight = w.to(x.device)
|
| 98 |
+
if not isinstance(w, GGMLTensor):
|
| 99 |
+
return F.linear(x, w, self.bias)
|
| 100 |
+
w = _dequant_ggml(w, x.dtype)
|
| 101 |
+
if self.weight_perm is not None:
|
| 102 |
+
if self.weight_perm.device != x.device:
|
| 103 |
+
self.weight_perm = self.weight_perm.to(x.device)
|
| 104 |
+
w = w[self.weight_perm] if self.weight_perm_dim == 0 else w[:, self.weight_perm]
|
| 105 |
+
return F.linear(x, w, self.bias)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class QuantizedEmbedding(nn.Module):
|
| 109 |
+
def __init__(self, num_embeddings, embedding_dim, dtype=torch.bfloat16):
|
| 110 |
+
super().__init__()
|
| 111 |
+
self.num_embeddings = num_embeddings
|
| 112 |
+
self.embedding_dim = embedding_dim
|
| 113 |
+
self.dtype = dtype
|
| 114 |
+
self.weight = None
|
| 115 |
+
|
| 116 |
+
def forward(self, x):
|
| 117 |
+
w = self.weight
|
| 118 |
+
if w is None:
|
| 119 |
+
w = torch.zeros(self.num_embeddings, self.embedding_dim, device=x.device, dtype=self.dtype)
|
| 120 |
+
if w.device != x.device:
|
| 121 |
+
w = self.weight = w.to(x.device)
|
| 122 |
+
if not isinstance(w, GGMLTensor):
|
| 123 |
+
return F.embedding(x, w)
|
| 124 |
+
w = _dequant_ggml(w, self.dtype)
|
| 125 |
+
return F.embedding(x, w)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _qperm_inv32() -> torch.Tensor:
|
| 129 |
+
return torch.tensor([(i % V_PER_KEY) * (V_HEADS // V_PER_KEY) + (i // V_PER_KEY)
|
| 130 |
+
for i in range(V_HEADS)], dtype=torch.long)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _qperm_vrows() -> torch.Tensor:
|
| 134 |
+
"""4096 行(32 头 x 128 维)value-major -> key-major 行序索引。"""
|
| 135 |
+
inv = _qperm_inv32()
|
| 136 |
+
return inv.repeat_interleave(128) * 128 + torch.arange(128).repeat(V_HEADS)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _qperm_qkv() -> torch.Tensor:
|
| 140 |
+
"""8192 行: 前 2 段 key(各 2048 行)保持,末段 4096 行 value 置换。"""
|
| 141 |
+
v_base = 2 * (V_HEADS // V_PER_KEY) * 128 # 4096: 两段 key 行数
|
| 142 |
+
return torch.cat([torch.arange(v_base), v_base + _qperm_vrows()])
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _dequant_ggml(w: "GGMLTensor", dtype: torch.dtype) -> torch.Tensor:
|
| 146 |
+
"""GGMLTensor -> fp16/bf16。先 as_subclass 转普通张量(零拷贝)再走 city96 反量化。
|
| 147 |
+
|
| 148 |
+
不能直接传 GGMLTensor: 其派生实例(reshape/view)无 tensor_shape 元数据,
|
| 149 |
+
.data 属性同样丢失元数据,会触发 shape property 的 AttributeError。
|
| 150 |
+
"""
|
| 151 |
+
from gguf import GGMLQuantizationType
|
| 152 |
+
try:
|
| 153 |
+
from .gguf_dequant import dequantize
|
| 154 |
+
except ImportError:
|
| 155 |
+
from gguf_dequant import dequantize
|
| 156 |
+
|
| 157 |
+
qtype = w.tensor_type
|
| 158 |
+
raw = w.as_subclass(torch.Tensor)
|
| 159 |
+
if qtype in (None, GGMLQuantizationType.F32, GGMLQuantizationType.F16):
|
| 160 |
+
return raw.to(dtype)
|
| 161 |
+
return dequantize(raw, qtype, w.tensor_shape, dtype=dtype).to(dtype)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _head_perm() -> torch.Tensor:
|
| 165 |
+
"""HF key-major head i -> GGUF value-major 位置: (i % 2) * 16 + (i // 2)。"""
|
| 166 |
+
return torch.tensor([(i % V_PER_KEY) * (V_HEADS // V_PER_KEY) + (i // V_PER_KEY)
|
| 167 |
+
for i in range(V_HEADS)], dtype=torch.long)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _v_reorder(w: torch.Tensor) -> torch.Tensor:
|
| 171 |
+
"""GGUF value-major -> HF key-major 行序逆置换(仅 linear_attn 的 v-head 参数)。"""
|
| 172 |
+
if w.numel() == V_HEADS:
|
| 173 |
+
return w[_head_perm()]
|
| 174 |
+
if w.dim() == 2 and w.shape[0] == V_HEADS:
|
| 175 |
+
return w[_head_perm()]
|
| 176 |
+
if w.dim() == 2 and w.shape[0] == V_HEADS * 128: # 4096 = 32 头 x 128 维
|
| 177 |
+
return w.view(V_HEADS, 128, -1)[_head_perm()].reshape(w.shape)
|
| 178 |
+
if w.dim() == 2 and w.shape[0] == 2 * V_HEADS * 128: # 8192: 前 2 段是 key,末段是 value
|
| 179 |
+
w = w.clone()
|
| 180 |
+
w[V_HEADS * 128:] = w[V_HEADS * 128:].view(V_HEADS, 128, -1)[_head_perm()].reshape(-1, w.shape[1])
|
| 181 |
+
return w
|
| 182 |
+
if w.dim() == 2 and w.shape[1] == V_HEADS * 128: # out_proj: value 维在列
|
| 183 |
+
return w.view(w.shape[0], V_HEADS, 128)[:, _head_perm(), :].reshape(w.shape)
|
| 184 |
+
return w
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _dequant(t, dtype: torch.dtype = torch.float16) -> torch.Tensor:
|
| 188 |
+
"""gguf ReaderTensor -> torch 反量化。
|
| 189 |
+
|
| 190 |
+
2D 张量按文件实际 [out, in] 行序 reshape;1D 按 t.shape。
|
| 191 |
+
"""
|
| 192 |
+
tt = torch.from_numpy(t.data)
|
| 193 |
+
tt.tensor_type = t.tensor_type
|
| 194 |
+
tt.tensor_shape = tuple(int(s) for s in t.shape)
|
| 195 |
+
w = dequantize_tensor(tt, dtype=dtype, dequant_dtype=torch.float32)
|
| 196 |
+
if len(t.shape) == 2:
|
| 197 |
+
return w.reshape(int(t.shape[1]), int(t.shape[0])).contiguous()
|
| 198 |
+
return w.reshape(tuple(int(s) for s in t.shape)).contiguous()
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _norm(w: torch.Tensor) -> torch.Tensor:
|
| 202 |
+
"""RMSNorm 增量约定: GGUF 真值 -> HF checkpoint 格式 (w - 1)。"""
|
| 203 |
+
return w - 1.0
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _map_gguf(path: str, quantized: bool = False):
|
| 207 |
+
"""GGUF -> HF state dict。quantized=True 时量化 2D 张量产出 GGMLTensor(驻留量化块)。
|
| 208 |
+
|
| 209 |
+
返回 (sd, perms, skipped)。perms: {hf_key: (perm_tensor, dim)} 反量化后的行/列置换。
|
| 210 |
+
"""
|
| 211 |
+
from gguf import GGUFReader, GGMLQuantizationType
|
| 212 |
+
|
| 213 |
+
reader = GGUFReader(path)
|
| 214 |
+
sd: dict[str, torch.Tensor] = {}
|
| 215 |
+
perms: dict[str, tuple[torch.Tensor, int]] = {}
|
| 216 |
+
skipped: list[str] = []
|
| 217 |
+
|
| 218 |
+
def qmat(t, hf_key: str, perm: torch.Tensor | None = None, dim: int = 0):
|
| 219 |
+
if quantized and t.tensor_type not in (None, GGMLQuantizationType.F32, GGMLQuantizationType.F16):
|
| 220 |
+
sd[hf_key] = GGMLTensor(torch.from_numpy(t.data), tensor_type=t.tensor_type,
|
| 221 |
+
tensor_shape=(int(t.shape[1]), int(t.shape[0])))
|
| 222 |
+
if perm is not None:
|
| 223 |
+
perms[hf_key] = (perm, dim)
|
| 224 |
+
else:
|
| 225 |
+
w = _dequant(t)
|
| 226 |
+
if perm is not None:
|
| 227 |
+
w = w[perm] if dim == 0 else w[:, perm]
|
| 228 |
+
sd[hf_key] = w
|
| 229 |
+
|
| 230 |
+
for t in reader.tensors:
|
| 231 |
+
n = t.name
|
| 232 |
+
if n == "token_embd.weight":
|
| 233 |
+
qmat(t, "language_model.embed_tokens.weight")
|
| 234 |
+
elif n == "output_norm.weight":
|
| 235 |
+
sd["language_model.norm.weight"] = _norm(_dequant(t))
|
| 236 |
+
elif n == "output.weight":
|
| 237 |
+
qmat(t, "language_model.lm_head.weight")
|
| 238 |
+
elif n.startswith("blk."):
|
| 239 |
+
parts = n.split(".")
|
| 240 |
+
i, rest = int(parts[1]), ".".join(parts[2:])
|
| 241 |
+
base = f"language_model.layers.{i}"
|
| 242 |
+
if rest == "attn_norm.weight":
|
| 243 |
+
sd[f"{base}.input_layernorm.weight"] = _norm(_dequant(t))
|
| 244 |
+
elif rest in ("post_attention_norm.weight", "attn_output_norm.weight"):
|
| 245 |
+
sd[f"{base}.post_attention_layernorm.weight"] = _norm(_dequant(t))
|
| 246 |
+
elif rest == "ffn_gate.weight":
|
| 247 |
+
qmat(t, f"{base}.mlp.gate_proj.weight")
|
| 248 |
+
elif rest == "ffn_up.weight":
|
| 249 |
+
qmat(t, f"{base}.mlp.up_proj.weight")
|
| 250 |
+
elif rest == "ffn_down.weight":
|
| 251 |
+
qmat(t, f"{base}.mlp.down_proj.weight")
|
| 252 |
+
elif rest == "attn_q.weight": # Q+Gate 融合,HF q_proj 同布局
|
| 253 |
+
qmat(t, f"{base}.self_attn.q_proj.weight")
|
| 254 |
+
elif rest == "attn_k.weight":
|
| 255 |
+
qmat(t, f"{base}.self_attn.k_proj.weight")
|
| 256 |
+
elif rest == "attn_v.weight":
|
| 257 |
+
qmat(t, f"{base}.self_attn.v_proj.weight")
|
| 258 |
+
elif rest == "attn_output.weight":
|
| 259 |
+
qmat(t, f"{base}.self_attn.o_proj.weight")
|
| 260 |
+
elif rest == "attn_q_norm.weight":
|
| 261 |
+
sd[f"{base}.self_attn.q_norm.weight"] = _norm(_dequant(t))
|
| 262 |
+
elif rest == "attn_k_norm.weight":
|
| 263 |
+
sd[f"{base}.self_attn.k_norm.weight"] = _norm(_dequant(t))
|
| 264 |
+
elif rest == "attn_qkv.weight": # 融合 qkv,v 段头序置换
|
| 265 |
+
qmat(t, f"{base}.linear_attn.in_proj_qkv.weight", perm=_qperm_qkv())
|
| 266 |
+
elif rest == "attn_gate.weight":
|
| 267 |
+
qmat(t, f"{base}.linear_attn.in_proj_z.weight", perm=_qperm_vrows())
|
| 268 |
+
elif rest == "ssm_conv1d.weight":
|
| 269 |
+
w = _v_reorder(_dequant(t)) # (conv_dim, 4) -> 深度卷积 (conv_dim, 1, 4)
|
| 270 |
+
sd[f"{base}.linear_attn.conv1d.weight"] = w.unsqueeze(1)
|
| 271 |
+
elif rest == "ssm_dt.bias":
|
| 272 |
+
sd[f"{base}.linear_attn.dt_bias"] = _v_reorder(_dequant(t))
|
| 273 |
+
elif rest in ("ssm_a", "ssm_a_noscan"):
|
| 274 |
+
sd[f"{base}.linear_attn.A_log"] = _v_reorder(-_dequant(t)).clamp_min(1e-12).log()
|
| 275 |
+
elif rest == "ssm_beta.weight":
|
| 276 |
+
qmat(t, f"{base}.linear_attn.in_proj_b.weight", perm=_qperm_inv32())
|
| 277 |
+
elif rest == "ssm_alpha.weight":
|
| 278 |
+
qmat(t, f"{base}.linear_attn.in_proj_a.weight", perm=_qperm_inv32())
|
| 279 |
+
elif rest == "ssm_norm.weight":
|
| 280 |
+
sd[f"{base}.linear_attn.norm.weight"] = _dequant(t) # Gated 范数,无增量约定
|
| 281 |
+
elif rest == "ssm_out.weight":
|
| 282 |
+
qmat(t, f"{base}.linear_attn.out_proj.weight", perm=_qperm_vrows(), dim=1)
|
| 283 |
+
else:
|
| 284 |
+
skipped.append(n)
|
| 285 |
+
else:
|
| 286 |
+
skipped.append(n)
|
| 287 |
+
|
| 288 |
+
if skipped:
|
| 289 |
+
print(f" [gguf] 忽略 {len(skipped)} 个未映射张量: {skipped[:6]}", flush=True)
|
| 290 |
+
return sd, perms, skipped
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def load_gguf_state_dict(path: str) -> dict[str, torch.Tensor]:
|
| 294 |
+
"""fp16 全展开模式(对比/调试用)。"""
|
| 295 |
+
sd, _, _ = _map_gguf(path, quantized=False)
|
| 296 |
+
return sd
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _build_model(config_path: str | None):
|
| 300 |
+
from transformers import AutoConfig, AutoModel
|
| 301 |
+
cfg = AutoConfig.from_pretrained(config_path or os.path.join(os.path.dirname(__file__), "qwen35_config.json"))
|
| 302 |
+
return AutoModel.from_config(cfg)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def build_quantized_model(state_dict: dict[str, torch.Tensor],
|
| 306 |
+
perms: dict[str, tuple[torch.Tensor, int]],
|
| 307 |
+
config_path: str | None = None):
|
| 308 |
+
"""量化模型: GGMLTensor 权重 + 前向时逐层反量化的 QuantizedLinear/Embedding。
|
| 309 |
+
|
| 310 |
+
只替换实际带量化权重的层;范数/conv1d 等普通张量走原层。
|
| 311 |
+
"""
|
| 312 |
+
model = _build_model(config_path)
|
| 313 |
+
quant_keys = {k for k, v in state_dict.items() if isinstance(v, GGMLTensor)}
|
| 314 |
+
|
| 315 |
+
def walk(module, prefix: str = ""):
|
| 316 |
+
for name, child in list(module.named_children()):
|
| 317 |
+
full = f"{prefix}.{name}" if prefix else name
|
| 318 |
+
if isinstance(child, nn.Linear) and f"{full}.weight" in quant_keys:
|
| 319 |
+
perm, dim = perms.get(f"{full}.weight", (None, None))
|
| 320 |
+
setattr(module, name, QuantizedLinear(child.in_features, child.out_features,
|
| 321 |
+
child.bias is not None, perm, dim))
|
| 322 |
+
elif isinstance(child, nn.Embedding) and f"{full}.weight" in quant_keys:
|
| 323 |
+
setattr(module, name, QuantizedEmbedding(child.num_embeddings, child.embedding_dim))
|
| 324 |
+
else:
|
| 325 |
+
walk(child, full)
|
| 326 |
+
|
| 327 |
+
walk(model)
|
| 328 |
+
for k, v in state_dict.items():
|
| 329 |
+
parts = k.split(".")
|
| 330 |
+
obj = model
|
| 331 |
+
for p in parts[:-1]:
|
| 332 |
+
obj = getattr(obj, p)
|
| 333 |
+
# GGMLTensor 不包 Parameter(推理无梯度,且 Parameter 会破坏量化元数据)
|
| 334 |
+
setattr(obj, parts[-1], v if isinstance(v, GGMLTensor)
|
| 335 |
+
else torch.nn.Parameter(v, requires_grad=False))
|
| 336 |
+
model_keys = set(model.state_dict()) | {k for k, v in state_dict.items() if isinstance(v, GGMLTensor)}
|
| 337 |
+
unexpected = sorted(set(state_dict) - model_keys)
|
| 338 |
+
return model, unexpected
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def load_gguf_state_dict(path: str) -> dict[str, torch.Tensor]:
|
| 342 |
+
from gguf import GGUFReader
|
| 343 |
+
|
| 344 |
+
reader = GGUFReader(path)
|
| 345 |
+
sd: dict[str, torch.Tensor] = {}
|
| 346 |
+
skipped: list[str] = []
|
| 347 |
+
|
| 348 |
+
for t in reader.tensors:
|
| 349 |
+
n = t.name
|
| 350 |
+
if n == "token_embd.weight":
|
| 351 |
+
sd["language_model.embed_tokens.weight"] = _dequant(t)
|
| 352 |
+
elif n == "output_norm.weight":
|
| 353 |
+
sd["language_model.norm.weight"] = _norm(_dequant(t))
|
| 354 |
+
elif n == "output.weight":
|
| 355 |
+
sd["language_model.lm_head.weight"] = _dequant(t) # 可能绑定 embed,可缺省
|
| 356 |
+
elif n.startswith("blk."):
|
| 357 |
+
parts = n.split(".")
|
| 358 |
+
i, rest = int(parts[1]), ".".join(parts[2:])
|
| 359 |
+
base = f"language_model.layers.{i}"
|
| 360 |
+
if rest == "attn_norm.weight":
|
| 361 |
+
sd[f"{base}.input_layernorm.weight"] = _norm(_dequant(t))
|
| 362 |
+
elif rest in ("post_attention_norm.weight", "attn_output_norm.weight"):
|
| 363 |
+
sd[f"{base}.post_attention_layernorm.weight"] = _norm(_dequant(t))
|
| 364 |
+
elif rest == "ffn_gate.weight":
|
| 365 |
+
sd[f"{base}.mlp.gate_proj.weight"] = _dequant(t)
|
| 366 |
+
elif rest == "ffn_up.weight":
|
| 367 |
+
sd[f"{base}.mlp.up_proj.weight"] = _dequant(t)
|
| 368 |
+
elif rest == "ffn_down.weight":
|
| 369 |
+
sd[f"{base}.mlp.down_proj.weight"] = _dequant(t)
|
| 370 |
+
elif rest == "attn_q.weight": # Q+Gate 融合,HF q_proj 同布局
|
| 371 |
+
sd[f"{base}.self_attn.q_proj.weight"] = _dequant(t)
|
| 372 |
+
elif rest == "attn_k.weight":
|
| 373 |
+
sd[f"{base}.self_attn.k_proj.weight"] = _dequant(t)
|
| 374 |
+
elif rest == "attn_v.weight":
|
| 375 |
+
sd[f"{base}.self_attn.v_proj.weight"] = _dequant(t)
|
| 376 |
+
elif rest == "attn_output.weight":
|
| 377 |
+
sd[f"{base}.self_attn.o_proj.weight"] = _dequant(t)
|
| 378 |
+
elif rest == "attn_q_norm.weight":
|
| 379 |
+
sd[f"{base}.self_attn.q_norm.weight"] = _norm(_dequant(t))
|
| 380 |
+
elif rest == "attn_k_norm.weight":
|
| 381 |
+
sd[f"{base}.self_attn.k_norm.weight"] = _norm(_dequant(t))
|
| 382 |
+
elif rest == "attn_qkv.weight": # 融合 qkv,HF in_proj_qkv 同布局
|
| 383 |
+
sd[f"{base}.linear_attn.in_proj_qkv.weight"] = _v_reorder(_dequant(t))
|
| 384 |
+
elif rest == "attn_gate.weight":
|
| 385 |
+
sd[f"{base}.linear_attn.in_proj_z.weight"] = _v_reorder(_dequant(t))
|
| 386 |
+
elif rest == "ssm_conv1d.weight":
|
| 387 |
+
w = _v_reorder(_dequant(t)) # (conv_dim, 4) -> 深度卷积 (conv_dim, 1, 4)
|
| 388 |
+
sd[f"{base}.linear_attn.conv1d.weight"] = w.unsqueeze(1)
|
| 389 |
+
elif rest == "ssm_dt.bias":
|
| 390 |
+
sd[f"{base}.linear_attn.dt_bias"] = _v_reorder(_dequant(t))
|
| 391 |
+
elif rest in ("ssm_a", "ssm_a_noscan"):
|
| 392 |
+
sd[f"{base}.linear_attn.A_log"] = _v_reorder(-_dequant(t)).clamp_min(1e-12).log()
|
| 393 |
+
elif rest == "ssm_beta.weight":
|
| 394 |
+
sd[f"{base}.linear_attn.in_proj_b.weight"] = _v_reorder(_dequant(t))
|
| 395 |
+
elif rest == "ssm_alpha.weight":
|
| 396 |
+
sd[f"{base}.linear_attn.in_proj_a.weight"] = _v_reorder(_dequant(t))
|
| 397 |
+
elif rest == "ssm_norm.weight":
|
| 398 |
+
sd[f"{base}.linear_attn.norm.weight"] = _dequant(t) # Gated 范数,无增量约定
|
| 399 |
+
elif rest == "ssm_out.weight":
|
| 400 |
+
sd[f"{base}.linear_attn.out_proj.weight"] = _v_reorder(_dequant(t))
|
| 401 |
+
else:
|
| 402 |
+
skipped.append(n)
|
| 403 |
+
else:
|
| 404 |
+
skipped.append(n)
|
| 405 |
+
|
| 406 |
+
if skipped:
|
| 407 |
+
print(f" [gguf] 忽略 {len(skipped)} 个未映射张量: {skipped[:6]}", flush=True)
|
| 408 |
+
return sd
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def build_hf_model(state_dict: dict[str, torch.Tensor], config_path: str | None = None):
|
| 412 |
+
"""按 state dict 构建 HF 模型(Qwen3_5Model)。
|
| 413 |
+
|
| 414 |
+
vision 权重缺省(text-only GGUF)属正常;仅用 language_model 子模块前向。
|
| 415 |
+
返回 (model, missing, unexpected)。
|
| 416 |
+
"""
|
| 417 |
+
from transformers import AutoConfig, AutoModel
|
| 418 |
+
|
| 419 |
+
cfg = AutoConfig.from_pretrained(config_path or os.path.join(os.path.dirname(__file__), "qwen35_config.json"))
|
| 420 |
+
model = AutoModel.from_config(cfg)
|
| 421 |
+
missing, unexpected = model.load_state_dict(state_dict, strict=False)
|
| 422 |
+
return model, missing, unexpected
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def load_gguf_model(path: str, config_path: str | None = None):
|
| 426 |
+
"""fp16 全展开加载(对比/调试用)。返回 (model, info)。"""
|
| 427 |
+
sd, _, _ = _map_gguf(path, quantized=False)
|
| 428 |
+
model, missing, unexpected = build_hf_model(sd, config_path)
|
| 429 |
+
return model, {"keys": len(sd), "missing": len(missing), "unexpected": len(unexpected)}
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def load_gguf_model_quantized(path: str, config_path: str | None = None):
|
| 433 |
+
"""量化驻留加载(ComfyUI 生产路径): 量化块���驻,前向时逐层反量化。
|
| 434 |
+
|
| 435 |
+
embed/lm_head 在加载期 CPU 一次性反量化成 bf16(GPU 上 Q6_K 反量化的
|
| 436 |
+
float32 中间张量峰值达数 GB,远大于 1.27GB 常驻收益)。
|
| 437 |
+
返回 (model, info)。
|
| 438 |
+
"""
|
| 439 |
+
sd, perms, _ = _map_gguf(path, quantized=True)
|
| 440 |
+
for k in ("language_model.embed_tokens.weight", "language_model.lm_head.weight"):
|
| 441 |
+
v = sd.get(k)
|
| 442 |
+
if isinstance(v, GGMLTensor):
|
| 443 |
+
sd[k] = _dequant_ggml(v, torch.bfloat16)
|
| 444 |
+
model, unexpected = build_quantized_model(sd, perms, config_path)
|
| 445 |
+
return model, {"keys": len(sd), "missing": 0, "unexpected": len(unexpected), "quantized": True}
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/inspect_gguf.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""检查 GGUF 文件的张量布局(自定义加载器的映射依据)。
|
| 3 |
+
|
| 4 |
+
用法:
|
| 5 |
+
python inspect_gguf.py <模型.gguf> [--limit 200]
|
| 6 |
+
输出:
|
| 7 |
+
- 每个张量的名字 / 形状 / 量化类型(按层分组)
|
| 8 |
+
- 顶层张量(token_embd / output_norm / output)
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import sys
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main() -> None:
|
| 17 |
+
ap = argparse.ArgumentParser()
|
| 18 |
+
ap.add_argument("gguf_path")
|
| 19 |
+
ap.add_argument("--limit", type=int, default=0, help="只显示前 N 个张量(默认全量)")
|
| 20 |
+
args = ap.parse_args()
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from gguf import GGUFReader
|
| 24 |
+
except ImportError as e:
|
| 25 |
+
print(f"需要 gguf 包: pip install gguf ({e})")
|
| 26 |
+
sys.exit(1)
|
| 27 |
+
|
| 28 |
+
reader = GGUFReader(args.gguf_path)
|
| 29 |
+
tensors = reader.tensors
|
| 30 |
+
print(f"文件: {args.gguf_path}")
|
| 31 |
+
print(f"张量总数: {len(tensors)}")
|
| 32 |
+
try:
|
| 33 |
+
arch = reader.fields.get("general.architecture")
|
| 34 |
+
print(f"architecture: {arch.parts[-1] if arch else '?'}")
|
| 35 |
+
except Exception:
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
# 按层分组
|
| 39 |
+
from collections import OrderedDict
|
| 40 |
+
layers: dict[str, list] = OrderedDict()
|
| 41 |
+
top = []
|
| 42 |
+
for t in tensors:
|
| 43 |
+
name = t.name
|
| 44 |
+
if name.startswith("blk."):
|
| 45 |
+
layer = name.split(".")[1]
|
| 46 |
+
layers.setdefault(layer, []).append(t)
|
| 47 |
+
else:
|
| 48 |
+
top.append(t)
|
| 49 |
+
|
| 50 |
+
print(f"\n===== 顶层张量 ({len(top)}) =====")
|
| 51 |
+
for t in top:
|
| 52 |
+
print(f" {t.name} shape={list(t.shape)} {t.tensor_type.name}")
|
| 53 |
+
|
| 54 |
+
shown = 0
|
| 55 |
+
for layer, ts in layers.items():
|
| 56 |
+
print(f"\n===== 第 {layer} 层 ({len(ts)} 张量) =====")
|
| 57 |
+
for t in ts:
|
| 58 |
+
print(f" {t.name} shape={list(t.shape)} {t.tensor_type.name}")
|
| 59 |
+
shown += 1
|
| 60 |
+
if args.limit and shown >= args.limit:
|
| 61 |
+
print("...(截断)")
|
| 62 |
+
sys.exit(0)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
main()
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/nodes.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ComfyUI 节点: MiniMax H3 Adapter Loader(假 CLIP 注入)。
|
| 2 |
+
|
| 3 |
+
用法(T2V 工作流):
|
| 4 |
+
UNETLoader(fl2va) -> model
|
| 5 |
+
MiniMaxH3AdapterLoader(student=<Qwen3.5-4B 文件夹>, adapter=<adapter.safetensors>) -> clip
|
| 6 |
+
VAELoader(video_vae) -> vae
|
| 7 |
+
MiniMaxH3ImageToVideo(clip=clip, vae=vae, prompt=..., width, height, length) -> positive + latent
|
| 8 |
+
BasicGuider(model, positive) -> RandomNoise -> SamplerCustomAdvanced -> VAEDecode -> SaveVideo
|
| 9 |
+
|
| 10 |
+
原理:
|
| 11 |
+
ComfyUI 的 MiniMaxH3 DiT (comfy/ldm/minimax/model.py) 在
|
| 12 |
+
preprocess_text_embeds / _forward 中条件式跳过投影:
|
| 13 |
+
if text_states.shape[-1] != self.hidden_size: # 5376
|
| 14 |
+
text_states = token_refiner(condition_proj(text_states))
|
| 15 |
+
适配器输出已是 [1, S_T, 5376](post-refiner),DiT 直接使用,零手术。
|
| 16 |
+
|
| 17 |
+
官方 MiniMaxH3ImageToVideo 只对 clip 调用 tokenize(prompt) +
|
| 18 |
+
encode_from_tokens_scheduled(tokens),故用鸭子类型 CLIP 对象替换即可,
|
| 19 |
+
latent 创建 / keyframe / duration 网格全部保留。
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import os
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
import folder_paths
|
| 28 |
+
import comfy.utils
|
| 29 |
+
import comfy.model_management as mm
|
| 30 |
+
|
| 31 |
+
from .adapter.model import H3Adapter
|
| 32 |
+
from .adapter_clip import MiniMaxH3AdapterCLIP, MiniMaxH3AdapterFromCLIP
|
| 33 |
+
from .student import StudentTextEncoder, get_torch_device
|
| 34 |
+
|
| 35 |
+
ADAPTER_FOLDER = "minimax_h3_adapter" # models/minimax_h3_adapter/
|
| 36 |
+
STUDENT_FOLDER = "minimax_h3_student" # -> models/text_encoders/
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _h3_tokenizer() -> object:
|
| 40 |
+
from transformers import AutoTokenizer
|
| 41 |
+
tok_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tokenizer")
|
| 42 |
+
return AutoTokenizer.from_pretrained(tok_dir)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _student_folder_options() -> list[str]:
|
| 46 |
+
"""models/text_encoders/ 下的子目录(HF 格式)或 .gguf 文件(transformers GGUF 加载)。"""
|
| 47 |
+
opts = []
|
| 48 |
+
for root in folder_paths.get_folder_paths(STUDENT_FOLDER):
|
| 49 |
+
if os.path.isdir(root):
|
| 50 |
+
opts += [d for d in sorted(os.listdir(root))
|
| 51 |
+
if os.path.isdir(os.path.join(root, d)) and os.path.exists(os.path.join(root, d, "config.json"))]
|
| 52 |
+
opts += [f for f in sorted(os.listdir(root)) if f.endswith(".gguf")]
|
| 53 |
+
return opts or ["<把 Qwen3.5-4B 文件夹或 .gguf 放到 models/text_encoders/>"]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class MiniMaxH3AdapterLoader:
|
| 57 |
+
@classmethod
|
| 58 |
+
def INPUT_TYPES(cls):
|
| 59 |
+
return {"required": {
|
| 60 |
+
"student": (_student_folder_options(),),
|
| 61 |
+
"adapter": (folder_paths.get_filename_list(ADAPTER_FOLDER),),
|
| 62 |
+
}, "optional": {
|
| 63 |
+
# 留空 = 整卡;24GB 及以下填 "16GiB" 等启用层间 offload
|
| 64 |
+
"gpu_mem": ("STRING", {"default": ""}),
|
| 65 |
+
}}
|
| 66 |
+
|
| 67 |
+
RETURN_TYPES = ("CLIP",)
|
| 68 |
+
RETURN_NAMES = ("clip",)
|
| 69 |
+
FUNCTION = "load_adapter"
|
| 70 |
+
CATEGORY = "model/conditioning/minimax"
|
| 71 |
+
|
| 72 |
+
def load_adapter(self, student: str, adapter: str, gpu_mem: str = ""):
|
| 73 |
+
dev = get_torch_device()
|
| 74 |
+
model = _load_adapter_weights(adapter, dev)
|
| 75 |
+
|
| 76 |
+
student_root = folder_paths.get_folder_paths(STUDENT_FOLDER)[0]
|
| 77 |
+
student_dir = os.path.join(student_root, student)
|
| 78 |
+
stu = StudentTextEncoder(student_dir, lowvram=True, gpu_mem=gpu_mem)
|
| 79 |
+
|
| 80 |
+
tok = _h3_tokenizer()
|
| 81 |
+
clip = MiniMaxH3AdapterCLIP(model, stu, tok, dev)
|
| 82 |
+
return (clip,)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _load_adapter_weights(adapter: str, dev: torch.device) -> H3Adapter:
|
| 86 |
+
"""加载适配器 safetensors -> bf16 H3Adapter(容错 model. 前缀)。"""
|
| 87 |
+
adapter_path = folder_paths.get_full_path(ADAPTER_FOLDER, adapter)
|
| 88 |
+
sd = comfy.utils.load_torch_file(adapter_path)
|
| 89 |
+
if any(k.startswith("model.") for k in sd):
|
| 90 |
+
sd = {k[len("model."):]: v for k, v in sd.items()}
|
| 91 |
+
model = H3Adapter().to(torch.bfloat16)
|
| 92 |
+
missing, unexpected = model.load_state_dict(sd, strict=False)
|
| 93 |
+
assert not missing, f"adapter 缺 key: {missing[:10]}"
|
| 94 |
+
if unexpected:
|
| 95 |
+
print(f" [adapter] ignored {len(unexpected)} unexpected keys", flush=True)
|
| 96 |
+
model.to(dev).eval()
|
| 97 |
+
for m in model.modules():
|
| 98 |
+
if hasattr(m, "use_sdpa"):
|
| 99 |
+
m.use_sdpa = True
|
| 100 |
+
print(f" [adapter] loaded {adapter} ({sum(p.numel() for p in model.parameters())/1e9:.3f}B)", flush=True)
|
| 101 |
+
return model
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class MiniMaxH3AdapterFromCLIPLoader:
|
| 105 |
+
"""CLIP→CLIP 包装: 学生 CLIP(CLIPLoader GGUF 等)的 encode 输出经适配器 -> 可喂给官方 MiniMaxH3ImageToVideo。
|
| 106 |
+
|
| 107 |
+
工作流:
|
| 108 |
+
CLIPLoader(GGUF, 学生模型) -> clip
|
| 109 |
+
MiniMaxH3AdapterFromCLIPLoader(clip=clip, adapter=adapter_stage2.safetensors) -> clip'
|
| 110 |
+
MiniMaxH3ImageToVideo(clip=clip', ...) # 官方节点原样
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
@classmethod
|
| 114 |
+
def INPUT_TYPES(cls):
|
| 115 |
+
return {"required": {
|
| 116 |
+
"clip": ("CLIP",),
|
| 117 |
+
"adapter": (folder_paths.get_filename_list(ADAPTER_FOLDER),),
|
| 118 |
+
}}
|
| 119 |
+
|
| 120 |
+
RETURN_TYPES = ("CLIP",)
|
| 121 |
+
RETURN_NAMES = ("clip",)
|
| 122 |
+
FUNCTION = "wrap_clip"
|
| 123 |
+
CATEGORY = "model/conditioning/minimax"
|
| 124 |
+
|
| 125 |
+
def wrap_clip(self, clip, adapter):
|
| 126 |
+
dev = get_torch_device()
|
| 127 |
+
model = _load_adapter_weights(adapter, dev)
|
| 128 |
+
wrapped = MiniMaxH3AdapterFromCLIP(clip, model, _h3_tokenizer(), dev)
|
| 129 |
+
return (wrapped,)
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/qwen35_config.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Qwen3_5ForConditionalGeneration"
|
| 4 |
+
],
|
| 5 |
+
"image_token_id": 248056,
|
| 6 |
+
"model_type": "qwen3_5",
|
| 7 |
+
"text_config": {
|
| 8 |
+
"attention_bias": false,
|
| 9 |
+
"attention_dropout": 0.0,
|
| 10 |
+
"attn_output_gate": true,
|
| 11 |
+
"dtype": "bfloat16",
|
| 12 |
+
"eos_token_id": 248044,
|
| 13 |
+
"full_attention_interval": 4,
|
| 14 |
+
"head_dim": 256,
|
| 15 |
+
"hidden_act": "silu",
|
| 16 |
+
"hidden_size": 2560,
|
| 17 |
+
"initializer_range": 0.02,
|
| 18 |
+
"intermediate_size": 9216,
|
| 19 |
+
"layer_types": [
|
| 20 |
+
"linear_attention",
|
| 21 |
+
"linear_attention",
|
| 22 |
+
"linear_attention",
|
| 23 |
+
"full_attention",
|
| 24 |
+
"linear_attention",
|
| 25 |
+
"linear_attention",
|
| 26 |
+
"linear_attention",
|
| 27 |
+
"full_attention",
|
| 28 |
+
"linear_attention",
|
| 29 |
+
"linear_attention",
|
| 30 |
+
"linear_attention",
|
| 31 |
+
"full_attention",
|
| 32 |
+
"linear_attention",
|
| 33 |
+
"linear_attention",
|
| 34 |
+
"linear_attention",
|
| 35 |
+
"full_attention",
|
| 36 |
+
"linear_attention",
|
| 37 |
+
"linear_attention",
|
| 38 |
+
"linear_attention",
|
| 39 |
+
"full_attention",
|
| 40 |
+
"linear_attention",
|
| 41 |
+
"linear_attention",
|
| 42 |
+
"linear_attention",
|
| 43 |
+
"full_attention",
|
| 44 |
+
"linear_attention",
|
| 45 |
+
"linear_attention",
|
| 46 |
+
"linear_attention",
|
| 47 |
+
"full_attention",
|
| 48 |
+
"linear_attention",
|
| 49 |
+
"linear_attention",
|
| 50 |
+
"linear_attention",
|
| 51 |
+
"full_attention"
|
| 52 |
+
],
|
| 53 |
+
"linear_conv_kernel_dim": 4,
|
| 54 |
+
"linear_key_head_dim": 128,
|
| 55 |
+
"linear_num_key_heads": 16,
|
| 56 |
+
"linear_num_value_heads": 32,
|
| 57 |
+
"linear_value_head_dim": 128,
|
| 58 |
+
"max_position_embeddings": 262144,
|
| 59 |
+
"mlp_only_layers": [],
|
| 60 |
+
"model_type": "qwen3_5_text",
|
| 61 |
+
"mtp_num_hidden_layers": 1,
|
| 62 |
+
"mtp_use_dedicated_embeddings": false,
|
| 63 |
+
"num_attention_heads": 16,
|
| 64 |
+
"num_hidden_layers": 32,
|
| 65 |
+
"num_key_value_heads": 4,
|
| 66 |
+
"rms_norm_eps": 1e-06,
|
| 67 |
+
"tie_word_embeddings": true,
|
| 68 |
+
"use_cache": true,
|
| 69 |
+
"vocab_size": 248320,
|
| 70 |
+
"mamba_ssm_dtype": "float32",
|
| 71 |
+
"rope_parameters": {
|
| 72 |
+
"mrope_interleaved": true,
|
| 73 |
+
"mrope_section": [
|
| 74 |
+
11,
|
| 75 |
+
11,
|
| 76 |
+
10
|
| 77 |
+
],
|
| 78 |
+
"rope_type": "default",
|
| 79 |
+
"rope_theta": 10000000,
|
| 80 |
+
"partial_rotary_factor": 0.25
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"tie_word_embeddings": true,
|
| 84 |
+
"transformers_version": "4.57.0.dev0",
|
| 85 |
+
"video_token_id": 248057,
|
| 86 |
+
"vision_config": {
|
| 87 |
+
"deepstack_visual_indexes": [],
|
| 88 |
+
"depth": 24,
|
| 89 |
+
"hidden_act": "gelu_pytorch_tanh",
|
| 90 |
+
"hidden_size": 1024,
|
| 91 |
+
"in_channels": 3,
|
| 92 |
+
"initializer_range": 0.02,
|
| 93 |
+
"intermediate_size": 4096,
|
| 94 |
+
"model_type": "qwen3_5",
|
| 95 |
+
"num_heads": 16,
|
| 96 |
+
"num_position_embeddings": 2304,
|
| 97 |
+
"out_hidden_size": 2560,
|
| 98 |
+
"patch_size": 16,
|
| 99 |
+
"spatial_merge_size": 2,
|
| 100 |
+
"temporal_patch_size": 2
|
| 101 |
+
},
|
| 102 |
+
"vision_end_token_id": 248054,
|
| 103 |
+
"vision_start_token_id": 248053
|
| 104 |
+
}
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/student.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Qwen3.5-4B 学生模型加载 + 隐藏态提取 + ComfyUI 感知的显存 offload。
|
| 2 |
+
|
| 3 |
+
load_student 移植自训练管线 extract_student_features.py:
|
| 4 |
+
- 修复 checkpoint 的 `model.` 多余前缀(否则全层随机初始化)
|
| 5 |
+
- 隐藏态取 hidden_states[-1](tie_last_hidden_states -> post-final-norm,与训练/评估同源)
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
from collections import defaultdict
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
try: # ComfyUI 内运行
|
| 16 |
+
import comfy.model_management as comfy_mm
|
| 17 |
+
|
| 18 |
+
def get_torch_device() -> torch.device:
|
| 19 |
+
return comfy_mm.get_torch_device()
|
| 20 |
+
|
| 21 |
+
def soft_empty_cache() -> None:
|
| 22 |
+
comfy_mm.soft_empty_cache()
|
| 23 |
+
except ImportError: # 独立测试环境
|
| 24 |
+
def get_torch_device() -> torch.device:
|
| 25 |
+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 26 |
+
|
| 27 |
+
def soft_empty_cache() -> None:
|
| 28 |
+
if torch.cuda.is_available():
|
| 29 |
+
torch.cuda.empty_cache()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def load_student(model_dir: str) -> torch.nn.Module:
|
| 33 |
+
"""加载 Qwen3.5-4B 权重(CPU),剥离 `model.` 前缀。
|
| 34 |
+
|
| 35 |
+
model_dir 为 .gguf 文件时走自研加载器(gguf_qwen35):
|
| 36 |
+
读取 + 反量化 + 张量名映射均自实现,不依赖 transformers 的 GGUF 支持,
|
| 37 |
+
产出与训练 checkpoint 同格式的 state dict。
|
| 38 |
+
"""
|
| 39 |
+
from safetensors import safe_open
|
| 40 |
+
from transformers import AutoConfig, AutoModel
|
| 41 |
+
|
| 42 |
+
if model_dir.endswith(".gguf"):
|
| 43 |
+
try:
|
| 44 |
+
from . import gguf_qwen35
|
| 45 |
+
except ImportError:
|
| 46 |
+
import gguf_qwen35
|
| 47 |
+
# 生产路径: 量化块驻留 + 前向逐层反量化(显存 ≈ 量化大小而非 fp16 展开)
|
| 48 |
+
model, info = gguf_qwen35.load_gguf_model_quantized(model_dir)
|
| 49 |
+
print(f" [student] GGUF loaded: {info}", flush=True)
|
| 50 |
+
return model
|
| 51 |
+
|
| 52 |
+
cfg = AutoConfig.from_pretrained(model_dir)
|
| 53 |
+
model = AutoModel.from_config(cfg) # 无权重实例化,避免 from_pretrained 错误初始化
|
| 54 |
+
index = json.load(open(os.path.join(model_dir, "model.safetensors.index.json")))
|
| 55 |
+
by_shard: dict[str, list[str]] = defaultdict(list)
|
| 56 |
+
for k, sh in index["weight_map"].items():
|
| 57 |
+
by_shard[sh].append(k)
|
| 58 |
+
sd: dict[str, torch.Tensor] = {}
|
| 59 |
+
for shard, keys in by_shard.items():
|
| 60 |
+
with safe_open(os.path.join(model_dir, shard), framework="pt", device="cpu") as sf:
|
| 61 |
+
for k in keys:
|
| 62 |
+
newk = k[len("model."):] if k.startswith("model.") else k
|
| 63 |
+
sd[newk] = sf.get_tensor(k)
|
| 64 |
+
missing, unexpected = model.load_state_dict(sd, strict=False)
|
| 65 |
+
del sd
|
| 66 |
+
assert not missing, f"missing after prefix strip: {missing[:10]}"
|
| 67 |
+
if unexpected:
|
| 68 |
+
print(f" [student] ignored {len(unexpected)} unexpected keys (e.g. {unexpected[:3]})", flush=True)
|
| 69 |
+
return model
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def find_language_model(model: torch.nn.Module) -> torch.nn.Module:
|
| 73 |
+
for name in ("language_model", "model", "text_model"):
|
| 74 |
+
if hasattr(model, name):
|
| 75 |
+
sub = getattr(model, name)
|
| 76 |
+
if hasattr(sub, "layers") or hasattr(sub, "config"):
|
| 77 |
+
return sub
|
| 78 |
+
raise RuntimeError(f"cannot locate language model submodule; attrs={[n for n in dir(model) if not n.startswith('_')]}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _move_plain_tensors(module: torch.nn.Module, device: torch.device) -> None:
|
| 82 |
+
"""迁移普通 tensor 属性(GGMLTensor 量化权重/embed 权重等非 Parameter/buffer)。
|
| 83 |
+
|
| 84 |
+
nn.Module.to() 只迁移 Parameter 与 registered buffer,量化权重是普通属性,
|
| 85 |
+
必须手动搬——否则 encode 后 offload 不彻底,量化权重滞留 GPU。
|
| 86 |
+
"""
|
| 87 |
+
for name, attr in list(module.__dict__.items()):
|
| 88 |
+
if isinstance(attr, torch.Tensor) and not isinstance(attr, torch.nn.Parameter):
|
| 89 |
+
setattr(module, name, attr.to(device))
|
| 90 |
+
for child in module.children():
|
| 91 |
+
_move_plain_tensors(child, device)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class StudentTextEncoder:
|
| 95 |
+
"""学生模型封装: prompt -> [S_S, 2560] bf16 hidden(post-final-norm)。
|
| 96 |
+
|
| 97 |
+
gpu_mem=""(默认): 整模型进 GPU;encode 后 lowvram 搬回 CPU。
|
| 98 |
+
gpu_mem="5GiB"/"16GiB" 等: accelerate 层间 offload(权重驻留 RAM、按层流式进 GPU),
|
| 99 |
+
适合 24GB 及以下卡(33B DiT + 4B 学生错峰)。
|
| 100 |
+
lowvram=True(默认): encode 完成后立即释放 GPU 占用,让位给 DiT 采样。
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
def __init__(self, model_dir: str, dtype: torch.dtype = torch.bfloat16,
|
| 104 |
+
lowvram: bool = True, gpu_mem: str = ""):
|
| 105 |
+
self.model_dir = model_dir
|
| 106 |
+
self.dtype = dtype
|
| 107 |
+
self.lowvram = lowvram
|
| 108 |
+
self.gpu_mem = gpu_mem
|
| 109 |
+
self._dispatched = False
|
| 110 |
+
self._model: torch.nn.Module | None = None
|
| 111 |
+
self._lm: torch.nn.Module | None = None
|
| 112 |
+
self._tok = None
|
| 113 |
+
|
| 114 |
+
def _ensure_ready(self):
|
| 115 |
+
if self._model is None:
|
| 116 |
+
from transformers import AutoTokenizer
|
| 117 |
+
model = load_student(self.model_dir).to(self.dtype)
|
| 118 |
+
if self.gpu_mem:
|
| 119 |
+
from accelerate import dispatch_model, infer_auto_device_map
|
| 120 |
+
max_memory = {"cpu": "20GiB", 0: self.gpu_mem}
|
| 121 |
+
no_split = getattr(model, "_no_split_modules", None) or None
|
| 122 |
+
device_map = infer_auto_device_map(model, max_memory=max_memory,
|
| 123 |
+
no_split_module_classes=no_split)
|
| 124 |
+
dispatch_model(model, device_map=device_map)
|
| 125 |
+
self._dispatched = True
|
| 126 |
+
self._model = model
|
| 127 |
+
self._lm = find_language_model(self._model)
|
| 128 |
+
if self.model_dir.endswith(".gguf"):
|
| 129 |
+
tok_dir = os.path.dirname(self.model_dir)
|
| 130 |
+
if not os.path.exists(os.path.join(tok_dir, "tokenizer.json")):
|
| 131 |
+
raise RuntimeError(
|
| 132 |
+
f"GGUF 同目录 {tok_dir} 缺少 tokenizer.json —— 请把 Qwen3.5-4B 的 tokenizer 文件"
|
| 133 |
+
"(tokenizer.json / tokenizer_config.json / vocab.json / merges.txt / chat_template.jinja)拷到该目录")
|
| 134 |
+
self._tok = AutoTokenizer.from_pretrained(tok_dir)
|
| 135 |
+
else:
|
| 136 |
+
self._tok = AutoTokenizer.from_pretrained(self.model_dir)
|
| 137 |
+
print(f" [student] loaded {self.model_dir} (gpu_mem={self.gpu_mem or 'whole'})", flush=True)
|
| 138 |
+
dev = get_torch_device()
|
| 139 |
+
if not self._dispatched and next(self._lm.parameters()).device != dev:
|
| 140 |
+
self._model.to(dev)
|
| 141 |
+
|
| 142 |
+
def offload(self) -> None:
|
| 143 |
+
if self._lm is None:
|
| 144 |
+
return
|
| 145 |
+
if self._dispatched:
|
| 146 |
+
soft_empty_cache()
|
| 147 |
+
return
|
| 148 |
+
self._model.to("cpu")
|
| 149 |
+
_move_plain_tensors(self._model, torch.device("cpu"))
|
| 150 |
+
soft_empty_cache()
|
| 151 |
+
|
| 152 |
+
@torch.no_grad()
|
| 153 |
+
def __call__(self, prompt: str) -> torch.Tensor:
|
| 154 |
+
self._ensure_ready()
|
| 155 |
+
dev = get_torch_device()
|
| 156 |
+
ids = self._tok(prompt, add_special_tokens=False)["input_ids"]
|
| 157 |
+
out = self._lm(input_ids=torch.tensor([ids], device=dev), output_hidden_states=True)
|
| 158 |
+
h = out.hidden_states[-1][0].to(self.dtype) # [S_S, 2560] post-final-norm
|
| 159 |
+
if self.lowvram:
|
| 160 |
+
self.offload()
|
| 161 |
+
return h
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/test_fake_clip.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""本地冒烟: 验证 MiniMaxH3AdapterCLIP 契约(无 ComfyUI 依赖也可跑)。
|
| 3 |
+
|
| 4 |
+
模式:
|
| 5 |
+
python test_fake_clip.py # quick: 假 student_fn,验证契约/形状/tags/透传
|
| 6 |
+
python test_fake_clip.py --real-student <qwen35 目录> # 真实 4B: 全链路 + offload 后显存回落
|
| 7 |
+
|
| 8 |
+
验证点:
|
| 9 |
+
1. tokenize(prompt) 捕获官方节点传入的 prompt
|
| 10 |
+
2. encode_from_tokens_scheduled -> CONDITIONING [[embeds, {"minimax_token_tags": tags}]]
|
| 11 |
+
- embeds [1, S_T, 5376] fp16 finite, tags [S_T] 全 1 long, S_T == len(h3 ids)
|
| 12 |
+
3. 5376 透传: 复刻 ComfyUI MiniMaxH3Model.preprocess_text_embeds —— shape[-1]==5376 恒等
|
| 13 |
+
4. add_dict 路径 (conditioning_set_values)
|
| 14 |
+
5. --real-student: encode 后学生模型已 offload(GPU 显存回落)
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
|
| 24 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 25 |
+
sys.path.insert(0, HERE)
|
| 26 |
+
|
| 27 |
+
from adapter.model import H3Adapter # noqa: E402
|
| 28 |
+
from student import StudentTextEncoder, get_torch_device, soft_empty_cache # noqa: E402
|
| 29 |
+
|
| 30 |
+
PROMPTS = [
|
| 31 |
+
"A cinematic shot of a tiny robot repairing a broken music box in a giant's attic, dust motes floating in the light",
|
| 32 |
+
"Close-up of papercraft hands folding an origami crane, warm studio light, shallow depth of field",
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def comfy_preprocess_text_embeds(text_states, hidden_size: int = 5376):
|
| 37 |
+
"""复刻 comfy/ldm/minimax/model.py preprocess_text_embeds(仅判定分支)。"""
|
| 38 |
+
if text_states.shape[-1] == hidden_size:
|
| 39 |
+
return text_states
|
| 40 |
+
raise AssertionError("输入不是 5376 维 -> 会被 condition_proj+token_refiner 处理(不应发生)")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def conditioning_set_values(cond, new_values):
|
| 44 |
+
"""node_helpers.conditioning_set_values 的 torch 版(无 ComfyUI 依赖)。"""
|
| 45 |
+
if len(cond) == 1 and len(cond[0][1]) == 0:
|
| 46 |
+
return [[cond[0][0], new_values]]
|
| 47 |
+
return [[c[0], {**c[1], **new_values}] for c in cond]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def run_checks(clip, prompts, tag: str) -> int:
|
| 51 |
+
fails = 0
|
| 52 |
+
for p in prompts:
|
| 53 |
+
clip.tokenize(p) # 官方节点会调 tokenize
|
| 54 |
+
cond = clip.encode_from_tokens_scheduled({})
|
| 55 |
+
assert len(cond) == 1, "CONDITIONING 应含 1 个元素"
|
| 56 |
+
embeds, extras = cond[0]
|
| 57 |
+
s_t = embeds.shape[1]
|
| 58 |
+
ids = clip._tok(p, add_special_tokens=False)["input_ids"]
|
| 59 |
+
|
| 60 |
+
checks = {
|
| 61 |
+
"embeds.shape == [1, S_T, 5376]": tuple(embeds.shape) == (1, s_t, 5376),
|
| 62 |
+
"S_T == len(h3 ids)": s_t == len(ids),
|
| 63 |
+
"dtype fp16": embeds.dtype == torch.float16,
|
| 64 |
+
"finite": bool(torch.isfinite(embeds).all()),
|
| 65 |
+
"extras 含 minimax_token_tags": "minimax_token_tags" in extras,
|
| 66 |
+
"tags 长度 == S_T": extras["minimax_token_tags"].shape[0] == s_t,
|
| 67 |
+
"tags 全 1": bool((extras["minimax_token_tags"] == 1).all()),
|
| 68 |
+
"tags dtype long": extras["minimax_token_tags"].dtype == torch.long,
|
| 69 |
+
}
|
| 70 |
+
# 5376 透传: DiT 应直接使用
|
| 71 |
+
passthrough = comfy_preprocess_text_embeds(embeds)
|
| 72 |
+
checks["preprocess_text_embeds 5376 透传恒等"] = torch.equal(passthrough, embeds)
|
| 73 |
+
|
| 74 |
+
for name, ok in checks.items():
|
| 75 |
+
print(f" [{'PASS' if ok else 'FAIL'}] {name}")
|
| 76 |
+
fails += 0 if ok else 1
|
| 77 |
+
print(f"--- {tag}: {'全部通过' if fails == 0 else f'{fails} 项失败'}")
|
| 78 |
+
return fails
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def check_wrapper_path(adapter, tok, dev) -> int:
|
| 82 |
+
"""验证 MiniMaxH3AdapterFromCLIP: 内层学生 CLIP 的 encode 输出被正确消费。"""
|
| 83 |
+
from adapter_clip import MiniMaxH3AdapterFromCLIP
|
| 84 |
+
|
| 85 |
+
class FakeInnerCLIP:
|
| 86 |
+
"""模拟 CLIPLoader(GGUF) 的 CLIP 对象契约(内层在 CPU 低显存模式——复现真实报错场景)。"""
|
| 87 |
+
def tokenize(self, prompt, images=None, **kwargs):
|
| 88 |
+
return {"g": prompt}
|
| 89 |
+
|
| 90 |
+
@torch.no_grad()
|
| 91 |
+
def encode_from_tokens(self, tokens, return_pooled=False, return_dict=False):
|
| 92 |
+
p = tokens["g"]
|
| 93 |
+
s_s = max(1, len(tok(p, add_special_tokens=False)["input_ids"]) // 2)
|
| 94 |
+
emb = torch.randn(1, s_s, 2560, dtype=torch.float16, device="cpu") * 0.1 # CPU!
|
| 95 |
+
return [[emb, {}]]
|
| 96 |
+
|
| 97 |
+
wrapped = MiniMaxH3AdapterFromCLIP(FakeInnerCLIP(), adapter, tok, dev)
|
| 98 |
+
assert next(adapter.parameters()).device != torch.device("cpu"), "测试前提: 适配器应在 GPU"
|
| 99 |
+
print("--- 包装器路径 (CLIPLoader(GGUF, CPU) -> 适配器(GPU) -> 官方节点):")
|
| 100 |
+
return run_checks(wrapped, PROMPTS, "CLIP→CLIP 包装(内层CPU)")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def main() -> None:
|
| 104 |
+
ap = argparse.ArgumentParser()
|
| 105 |
+
ap.add_argument("--real-student", default="", help="Qwen3.5-4B 目录(真实全链路 + offload 验证)")
|
| 106 |
+
ap.add_argument("--device", default="")
|
| 107 |
+
args = ap.parse_args()
|
| 108 |
+
|
| 109 |
+
dev = torch.device(args.device) if args.device else get_torch_device()
|
| 110 |
+
print(f"device: {dev}")
|
| 111 |
+
|
| 112 |
+
# 适配器: 随机初始化(机制验证;真实权重在云端导出的 safetensors)
|
| 113 |
+
adapter = H3Adapter().to(torch.bfloat16).to(dev).eval()
|
| 114 |
+
for m in adapter.modules():
|
| 115 |
+
if hasattr(m, "use_sdpa"):
|
| 116 |
+
m.use_sdpa = True
|
| 117 |
+
|
| 118 |
+
from transformers import AutoTokenizer
|
| 119 |
+
tok = AutoTokenizer.from_pretrained(os.path.join(HERE, "tokenizer"))
|
| 120 |
+
print(f"tokenizer: {type(tok).__name__} | vocab={tok.vocab_size}")
|
| 121 |
+
|
| 122 |
+
if args.real_student:
|
| 123 |
+
stu = StudentTextEncoder(args.real_student, lowvram=True, gpu_mem="5GiB")
|
| 124 |
+
tag = "真实学生模型(dispatch offload)"
|
| 125 |
+
else:
|
| 126 |
+
# 假 student_fn: 返回随机 [S_S, 2560](S_S 与 prompt token 数成比例)
|
| 127 |
+
def fake_student(prompt: str) -> torch.Tensor:
|
| 128 |
+
s_s = max(1, len(tok(prompt, add_special_tokens=False)["input_ids"]) // 2)
|
| 129 |
+
return torch.randn(s_s, 2560, dtype=torch.bfloat16, device=dev) * 0.1
|
| 130 |
+
stu = type("FakeStudent", (), {"__call__": staticmethod(fake_student)})()
|
| 131 |
+
tag = "假学生模型(机制验证)"
|
| 132 |
+
|
| 133 |
+
from adapter_clip import MiniMaxH3AdapterCLIP
|
| 134 |
+
clip = MiniMaxH3AdapterCLIP(adapter, stu, tok, dev)
|
| 135 |
+
|
| 136 |
+
fails = run_checks(clip, PROMPTS, tag)
|
| 137 |
+
fails += check_wrapper_path(adapter, tok, dev)
|
| 138 |
+
|
| 139 |
+
# add_dict 路径(模拟官方节点传入 keyframes 等 extras)
|
| 140 |
+
clip.tokenize(PROMPTS[0])
|
| 141 |
+
cond = clip.encode_from_tokens_scheduled({}, add_dict={"minimax_frame_count": 124})
|
| 142 |
+
assert "minimax_frame_count" in cond[0][1], "add_dict 未并入 extras"
|
| 143 |
+
print(" [PASS] add_dict 并入 conditioning extras")
|
| 144 |
+
|
| 145 |
+
if args.real_student:
|
| 146 |
+
# offload 验证: 整卡模式 -> 已搬回 CPU; dispatch 模式 -> 权重层间驻留(首层在 CPU)
|
| 147 |
+
lm = stu._lm
|
| 148 |
+
on_cpu = next(lm.parameters()).device.type == "cpu" or stu._dispatched
|
| 149 |
+
print(f" [{'PASS' if on_cpu else 'FAIL'}] encode 后学生模型已释放 GPU 常驻 (dispatched={stu._dispatched})")
|
| 150 |
+
fails += 0 if on_cpu else 1
|
| 151 |
+
if torch.cuda.is_available():
|
| 152 |
+
free = torch.cuda.mem_get_info()[0] / 2 ** 30
|
| 153 |
+
print(f" [info] encode 后 GPU 可用显存 {free:.1f} GiB")
|
| 154 |
+
|
| 155 |
+
print("SMOKE", "PASS ✅" if fails == 0 else f"FAIL ({fails})")
|
| 156 |
+
raise SystemExit(0 if fails == 0 else 1)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
main()
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/test_gguf_loader.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""合成迷你 GGUF 回环测试: 复刻真实 HauhauCS GGUF 的全部布局约定
|
| 3 |
+
(2D 数据 [out,in] + 元数据 [in,out]、v-head value-major 置换、范数增量 -1、ssm_a 存 -A),
|
| 4 |
+
验证 gguf_qwen35 加载器的映射/反量化/置换/变换正确,含数值断言(cos)。
|
| 5 |
+
|
| 6 |
+
量化类型用 Q8_0(gguf 库支持量化;Q4_K 反量化已由真实文件 426/426 逐张量验证)。
|
| 7 |
+
|
| 8 |
+
用法: python test_gguf_loader.py [--keep](--keep 保留临时文件供 inspect_gguf 查看)
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import tempfile
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 21 |
+
sys.path.insert(0, HERE)
|
| 22 |
+
|
| 23 |
+
N_EMBD = 2560
|
| 24 |
+
FFN = 9216
|
| 25 |
+
N_HEAD, N_HEAD_KV, HEAD_DIM = 16, 4, 256
|
| 26 |
+
L_KEY_HEADS, L_KEY_HD, L_V_HEADS, L_V_HD = 16, 128, 32, 128
|
| 27 |
+
CONV_K = 4
|
| 28 |
+
|
| 29 |
+
GGUF_TO_HF = np.array([(i % 16) * 2 + (i // 16) for i in range(32)], dtype=np.int64)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def hf_to_gguf(x):
|
| 33 |
+
return x[GGUF_TO_HF]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def w2d(rng, out, inn):
|
| 37 |
+
return rng.standard_normal((out, inn)).astype(np.float32)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def add2d(w, name, data, qtype=None):
|
| 41 |
+
from gguf.quants import quantize
|
| 42 |
+
from gguf import GGML_QUANT_SIZES
|
| 43 |
+
out, inn = int(data.shape[0]), int(data.shape[1])
|
| 44 |
+
if qtype is not None:
|
| 45 |
+
data = quantize(np.ascontiguousarray(data), qtype)
|
| 46 |
+
block, tsize = GGML_QUANT_SIZES[qtype]
|
| 47 |
+
w.add_tensor(name, data, raw_shape=(out, inn * tsize // block), raw_dtype=qtype)
|
| 48 |
+
else:
|
| 49 |
+
w.add_tensor(name, data, raw_shape=(out, inn))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def add1d(w, name, data):
|
| 53 |
+
w.add_tensor(name, data)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def add_layer(w, rng, i: int, is_full: bool, expect: dict):
|
| 57 |
+
base = f"blk.{i}"
|
| 58 |
+
pref = f"language_model.layers.{i}."
|
| 59 |
+
norm_in = w2d(rng, N_EMBD, 1).ravel() + 0.5
|
| 60 |
+
norm_out = w2d(rng, N_EMBD, 1).ravel() + 0.5
|
| 61 |
+
add1d(w, f"{base}.attn_norm.weight", norm_in)
|
| 62 |
+
add1d(w, f"{base}.post_attention_norm.weight", norm_out)
|
| 63 |
+
expect[f"{pref}input_layernorm.weight"] = norm_in - 1.0
|
| 64 |
+
expect[f"{pref}post_attention_layernorm.weight"] = norm_out - 1.0
|
| 65 |
+
gate = w2d(rng, FFN, N_EMBD)
|
| 66 |
+
up = w2d(rng, FFN, N_EMBD)
|
| 67 |
+
down = w2d(rng, N_EMBD, FFN)
|
| 68 |
+
add2d(w, f"{base}.ffn_gate.weight", gate, qtype=Q8)
|
| 69 |
+
add2d(w, f"{base}.ffn_up.weight", up, qtype=Q8)
|
| 70 |
+
add2d(w, f"{base}.ffn_down.weight", down, qtype=Q8)
|
| 71 |
+
expect[f"{pref}mlp.gate_proj.weight"] = gate
|
| 72 |
+
expect[f"{pref}mlp.up_proj.weight"] = up
|
| 73 |
+
expect[f"{pref}mlp.down_proj.weight"] = down
|
| 74 |
+
if is_full:
|
| 75 |
+
q = w2d(rng, 2 * N_HEAD * HEAD_DIM, N_EMBD)
|
| 76 |
+
k = w2d(rng, N_HEAD_KV * HEAD_DIM, N_EMBD)
|
| 77 |
+
v = w2d(rng, N_HEAD_KV * HEAD_DIM, N_EMBD)
|
| 78 |
+
o = w2d(rng, N_EMBD, N_HEAD * HEAD_DIM)
|
| 79 |
+
qn = w2d(rng, HEAD_DIM, 1).ravel() + 0.5
|
| 80 |
+
kn = w2d(rng, HEAD_DIM, 1).ravel() + 0.5
|
| 81 |
+
add2d(w, f"{base}.attn_q.weight", q, qtype=Q8)
|
| 82 |
+
add2d(w, f"{base}.attn_k.weight", k, qtype=Q8)
|
| 83 |
+
add2d(w, f"{base}.attn_v.weight", v, qtype=Q8)
|
| 84 |
+
add2d(w, f"{base}.attn_output.weight", o, qtype=Q8)
|
| 85 |
+
add1d(w, f"{base}.attn_q_norm.weight", qn)
|
| 86 |
+
add1d(w, f"{base}.attn_k_norm.weight", kn)
|
| 87 |
+
expect[f"{pref}self_attn.q_proj.weight"] = q
|
| 88 |
+
expect[f"{pref}self_attn.k_proj.weight"] = k
|
| 89 |
+
expect[f"{pref}self_attn.v_proj.weight"] = v
|
| 90 |
+
expect[f"{pref}self_attn.o_proj.weight"] = o
|
| 91 |
+
expect[f"{pref}self_attn.q_norm.weight"] = qn - 1.0
|
| 92 |
+
expect[f"{pref}self_attn.k_norm.weight"] = kn - 1.0
|
| 93 |
+
else:
|
| 94 |
+
k_dim, v_dim = L_KEY_HEADS * L_KEY_HD, L_V_HEADS * L_V_HD
|
| 95 |
+
conv_dim = k_dim * 2 + v_dim
|
| 96 |
+
qkv = w2d(rng, conv_dim, N_EMBD)
|
| 97 |
+
qkv_hf = qkv.copy()
|
| 98 |
+
qkv[v_dim:] = hf_to_gguf(qkv[v_dim:].reshape(32, 128, -1)).reshape(v_dim, -1)
|
| 99 |
+
z = w2d(rng, v_dim, N_EMBD)
|
| 100 |
+
z_hf = z.copy()
|
| 101 |
+
z = hf_to_gguf(z.reshape(32, 128, -1)).reshape(v_dim, -1)
|
| 102 |
+
conv = w2d(rng, conv_dim, CONV_K)
|
| 103 |
+
conv_hf = conv.copy()
|
| 104 |
+
conv[v_dim:] = hf_to_gguf(conv[v_dim:].reshape(32, 128, -1)).reshape(v_dim, -1)
|
| 105 |
+
dt_hf = rng.standard_normal(L_V_HEADS).astype(np.float32)
|
| 106 |
+
dt = hf_to_gguf(dt_hf)
|
| 107 |
+
a_log = rng.standard_normal(L_V_HEADS).astype(np.float32)
|
| 108 |
+
beta_hf = w2d(rng, L_V_HEADS, N_EMBD)
|
| 109 |
+
beta = hf_to_gguf(beta_hf)
|
| 110 |
+
alpha_hf = w2d(rng, L_V_HEADS, N_EMBD)
|
| 111 |
+
alpha = hf_to_gguf(alpha_hf)
|
| 112 |
+
s_norm = w2d(rng, L_V_HD, 1).ravel() + 0.5
|
| 113 |
+
out_proj = w2d(rng, N_EMBD, v_dim)
|
| 114 |
+
out_proj_gguf = out_proj.reshape(N_EMBD, 32, 128)[:, GGUF_TO_HF, :].reshape(N_EMBD, v_dim)
|
| 115 |
+
add2d(w, f"{base}.attn_qkv.weight", qkv, qtype=Q8)
|
| 116 |
+
add2d(w, f"{base}.attn_gate.weight", z, qtype=Q8)
|
| 117 |
+
w.add_tensor(f"{base}.ssm_conv1d.weight", conv, raw_shape=(conv_dim, CONV_K))
|
| 118 |
+
add1d(w, f"{base}.ssm_dt.bias", dt)
|
| 119 |
+
add1d(w, f"{base}.ssm_a", hf_to_gguf(-np.exp(a_log)).astype(np.float32))
|
| 120 |
+
add2d(w, f"{base}.ssm_beta.weight", beta, qtype=Q8)
|
| 121 |
+
add2d(w, f"{base}.ssm_alpha.weight", alpha, qtype=Q8)
|
| 122 |
+
add1d(w, f"{base}.ssm_norm.weight", s_norm)
|
| 123 |
+
add2d(w, f"{base}.ssm_out.weight", out_proj_gguf, qtype=Q8)
|
| 124 |
+
expect[f"{pref}linear_attn.in_proj_qkv.weight"] = qkv_hf
|
| 125 |
+
expect[f"{pref}linear_attn.in_proj_z.weight"] = z_hf
|
| 126 |
+
expect[f"{pref}linear_attn.conv1d.weight"] = conv_hf.reshape(conv_dim, 1, CONV_K)
|
| 127 |
+
expect[f"{pref}linear_attn.dt_bias"] = dt_hf
|
| 128 |
+
expect[f"{pref}linear_attn.A_log"] = a_log
|
| 129 |
+
expect[f"{pref}linear_attn.in_proj_b.weight"] = beta_hf
|
| 130 |
+
expect[f"{pref}linear_attn.in_proj_a.weight"] = alpha_hf
|
| 131 |
+
expect[f"{pref}linear_attn.norm.weight"] = s_norm
|
| 132 |
+
expect[f"{pref}linear_attn.out_proj.weight"] = out_proj
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def main() -> None:
|
| 136 |
+
global Q8
|
| 137 |
+
from gguf import GGUFWriter, GGMLQuantizationType
|
| 138 |
+
Q8 = GGMLQuantizationType.Q8_0
|
| 139 |
+
|
| 140 |
+
ap = argparse.ArgumentParser()
|
| 141 |
+
ap.add_argument("--keep", action="store_true")
|
| 142 |
+
args = ap.parse_args()
|
| 143 |
+
|
| 144 |
+
tmpdir = tempfile.mkdtemp(prefix="gguf_test_")
|
| 145 |
+
path = os.path.join(tmpdir, "toy_qwen35.gguf")
|
| 146 |
+
print(f"写入合成 GGUF -> {path}")
|
| 147 |
+
|
| 148 |
+
rng = np.random.default_rng(42)
|
| 149 |
+
expect: dict[str, np.ndarray] = {}
|
| 150 |
+
w = GGUFWriter(path, "qwen35")
|
| 151 |
+
emb = w2d(rng, 248320, N_EMBD)
|
| 152 |
+
add2d(w, "token_embd.weight", emb)
|
| 153 |
+
expect["language_model.embed_tokens.weight"] = emb
|
| 154 |
+
out_norm = w2d(rng, N_EMBD, 1).ravel() + 0.5
|
| 155 |
+
add1d(w, "output_norm.weight", out_norm)
|
| 156 |
+
expect["language_model.norm.weight"] = out_norm - 1.0
|
| 157 |
+
add_layer(w, rng, 0, is_full=False, expect=expect)
|
| 158 |
+
add_layer(w, rng, 3, is_full=True, expect=expect)
|
| 159 |
+
w.write_header_to_file()
|
| 160 |
+
w.write_kv_data_to_file()
|
| 161 |
+
w.write_tensors_to_file()
|
| 162 |
+
w.close()
|
| 163 |
+
print(f"GGUF 写入完成 ({os.path.getsize(path) / 1e6:.0f} MB)")
|
| 164 |
+
|
| 165 |
+
from gguf_qwen35 import load_gguf_state_dict, build_hf_model
|
| 166 |
+
sd = load_gguf_state_dict(path)
|
| 167 |
+
print(f"映射出 {len(sd)} 个键")
|
| 168 |
+
|
| 169 |
+
fails = 0
|
| 170 |
+
for key, want in sorted(expect.items()):
|
| 171 |
+
if key not in sd:
|
| 172 |
+
print(f" [FAIL] {key} MISSING")
|
| 173 |
+
fails += 1
|
| 174 |
+
continue
|
| 175 |
+
got = sd[key]
|
| 176 |
+
ws = torch.from_numpy(want).float()
|
| 177 |
+
gs = got.float()
|
| 178 |
+
if tuple(gs.shape) != tuple(ws.shape):
|
| 179 |
+
print(f" [FAIL] {key} shape {tuple(gs.shape)} 期望 {tuple(ws.shape)}")
|
| 180 |
+
fails += 1
|
| 181 |
+
continue
|
| 182 |
+
dot = torch.zeros((), dtype=torch.float64)
|
| 183 |
+
na = torch.zeros((), dtype=torch.float64)
|
| 184 |
+
nb = torch.zeros((), dtype=torch.float64)
|
| 185 |
+
CH = 1 << 22
|
| 186 |
+
ga, wf = gs.flatten(), ws.flatten()
|
| 187 |
+
for i in range(0, ga.numel(), CH):
|
| 188 |
+
x, y = ga[i:i + CH], wf[i:i + CH]
|
| 189 |
+
dot += (x * y).sum().double()
|
| 190 |
+
na += (x * x).sum().double()
|
| 191 |
+
nb += (y * y).sum().double()
|
| 192 |
+
c = float(dot / (na.sqrt() * nb.sqrt() + 1e-30))
|
| 193 |
+
ok = c > 0.99
|
| 194 |
+
print(f" [{'PASS' if ok else 'FAIL'}] {key} {tuple(gs.shape)} cos={c:.5f}")
|
| 195 |
+
fails += 0 if ok else 1
|
| 196 |
+
|
| 197 |
+
model, missing, unexpected = build_hf_model(sd, os.path.join(HERE, "qwen35_config.json"))
|
| 198 |
+
unexp_ok = len(unexpected) == 0
|
| 199 |
+
print(f" [{'PASS' if unexp_ok else 'FAIL'}] load_state_dict unexpected={len(unexpected)} (应=0)"
|
| 200 |
+
+ (f": {unexpected[:4]}" if unexpected else ""))
|
| 201 |
+
fails += 0 if unexp_ok else 1
|
| 202 |
+
print(f" [info] missing={len(missing)}(其余 30 层 + vision 属正常缺省)")
|
| 203 |
+
|
| 204 |
+
if not args.keep:
|
| 205 |
+
import shutil
|
| 206 |
+
shutil.rmtree(tmpdir)
|
| 207 |
+
print("GGUF 回环", "PASS ✅" if fails == 0 else f"FAIL ({fails})")
|
| 208 |
+
raise SystemExit(0 if fails == 0 else 1)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
if __name__ == "__main__":
|
| 212 |
+
main()
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/merges.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/tokenizer_config.json
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_bos_token": false,
|
| 3 |
+
"add_prefix_space": false,
|
| 4 |
+
"added_tokens_decoder": {
|
| 5 |
+
"151643": {
|
| 6 |
+
"content": "<|endoftext|>",
|
| 7 |
+
"lstrip": false,
|
| 8 |
+
"normalized": false,
|
| 9 |
+
"rstrip": false,
|
| 10 |
+
"single_word": false,
|
| 11 |
+
"special": true
|
| 12 |
+
},
|
| 13 |
+
"151644": {
|
| 14 |
+
"content": "<|im_start|>",
|
| 15 |
+
"lstrip": false,
|
| 16 |
+
"normalized": false,
|
| 17 |
+
"rstrip": false,
|
| 18 |
+
"single_word": false,
|
| 19 |
+
"special": true
|
| 20 |
+
},
|
| 21 |
+
"151645": {
|
| 22 |
+
"content": "<|im_end|>",
|
| 23 |
+
"lstrip": false,
|
| 24 |
+
"normalized": false,
|
| 25 |
+
"rstrip": false,
|
| 26 |
+
"single_word": false,
|
| 27 |
+
"special": true
|
| 28 |
+
},
|
| 29 |
+
"151646": {
|
| 30 |
+
"content": "<|object_ref_start|>",
|
| 31 |
+
"lstrip": false,
|
| 32 |
+
"normalized": false,
|
| 33 |
+
"rstrip": false,
|
| 34 |
+
"single_word": false,
|
| 35 |
+
"special": true
|
| 36 |
+
},
|
| 37 |
+
"151647": {
|
| 38 |
+
"content": "<|object_ref_end|>",
|
| 39 |
+
"lstrip": false,
|
| 40 |
+
"normalized": false,
|
| 41 |
+
"rstrip": false,
|
| 42 |
+
"single_word": false,
|
| 43 |
+
"special": true
|
| 44 |
+
},
|
| 45 |
+
"151648": {
|
| 46 |
+
"content": "<|box_start|>",
|
| 47 |
+
"lstrip": false,
|
| 48 |
+
"normalized": false,
|
| 49 |
+
"rstrip": false,
|
| 50 |
+
"single_word": false,
|
| 51 |
+
"special": true
|
| 52 |
+
},
|
| 53 |
+
"151649": {
|
| 54 |
+
"content": "<|box_end|>",
|
| 55 |
+
"lstrip": false,
|
| 56 |
+
"normalized": false,
|
| 57 |
+
"rstrip": false,
|
| 58 |
+
"single_word": false,
|
| 59 |
+
"special": true
|
| 60 |
+
},
|
| 61 |
+
"151650": {
|
| 62 |
+
"content": "<|quad_start|>",
|
| 63 |
+
"lstrip": false,
|
| 64 |
+
"normalized": false,
|
| 65 |
+
"rstrip": false,
|
| 66 |
+
"single_word": false,
|
| 67 |
+
"special": true
|
| 68 |
+
},
|
| 69 |
+
"151651": {
|
| 70 |
+
"content": "<|quad_end|>",
|
| 71 |
+
"lstrip": false,
|
| 72 |
+
"normalized": false,
|
| 73 |
+
"rstrip": false,
|
| 74 |
+
"single_word": false,
|
| 75 |
+
"special": true
|
| 76 |
+
},
|
| 77 |
+
"151652": {
|
| 78 |
+
"content": "<|vision_start|>",
|
| 79 |
+
"lstrip": false,
|
| 80 |
+
"normalized": false,
|
| 81 |
+
"rstrip": false,
|
| 82 |
+
"single_word": false,
|
| 83 |
+
"special": true
|
| 84 |
+
},
|
| 85 |
+
"151653": {
|
| 86 |
+
"content": "<|vision_end|>",
|
| 87 |
+
"lstrip": false,
|
| 88 |
+
"normalized": false,
|
| 89 |
+
"rstrip": false,
|
| 90 |
+
"single_word": false,
|
| 91 |
+
"special": true
|
| 92 |
+
},
|
| 93 |
+
"151654": {
|
| 94 |
+
"content": "<|vision_pad|>",
|
| 95 |
+
"lstrip": false,
|
| 96 |
+
"normalized": false,
|
| 97 |
+
"rstrip": false,
|
| 98 |
+
"single_word": false,
|
| 99 |
+
"special": true
|
| 100 |
+
},
|
| 101 |
+
"151655": {
|
| 102 |
+
"content": "<|image_pad|>",
|
| 103 |
+
"lstrip": false,
|
| 104 |
+
"normalized": false,
|
| 105 |
+
"rstrip": false,
|
| 106 |
+
"single_word": false,
|
| 107 |
+
"special": true
|
| 108 |
+
},
|
| 109 |
+
"151656": {
|
| 110 |
+
"content": "<|video_pad|>",
|
| 111 |
+
"lstrip": false,
|
| 112 |
+
"normalized": false,
|
| 113 |
+
"rstrip": false,
|
| 114 |
+
"single_word": false,
|
| 115 |
+
"special": true
|
| 116 |
+
},
|
| 117 |
+
"151657": {
|
| 118 |
+
"content": "<tool_call>",
|
| 119 |
+
"lstrip": false,
|
| 120 |
+
"normalized": false,
|
| 121 |
+
"rstrip": false,
|
| 122 |
+
"single_word": false,
|
| 123 |
+
"special": false
|
| 124 |
+
},
|
| 125 |
+
"151658": {
|
| 126 |
+
"content": "</tool_call>",
|
| 127 |
+
"lstrip": false,
|
| 128 |
+
"normalized": false,
|
| 129 |
+
"rstrip": false,
|
| 130 |
+
"single_word": false,
|
| 131 |
+
"special": false
|
| 132 |
+
},
|
| 133 |
+
"151659": {
|
| 134 |
+
"content": "<|fim_prefix|>",
|
| 135 |
+
"lstrip": false,
|
| 136 |
+
"normalized": false,
|
| 137 |
+
"rstrip": false,
|
| 138 |
+
"single_word": false,
|
| 139 |
+
"special": false
|
| 140 |
+
},
|
| 141 |
+
"151660": {
|
| 142 |
+
"content": "<|fim_middle|>",
|
| 143 |
+
"lstrip": false,
|
| 144 |
+
"normalized": false,
|
| 145 |
+
"rstrip": false,
|
| 146 |
+
"single_word": false,
|
| 147 |
+
"special": false
|
| 148 |
+
},
|
| 149 |
+
"151661": {
|
| 150 |
+
"content": "<|fim_suffix|>",
|
| 151 |
+
"lstrip": false,
|
| 152 |
+
"normalized": false,
|
| 153 |
+
"rstrip": false,
|
| 154 |
+
"single_word": false,
|
| 155 |
+
"special": false
|
| 156 |
+
},
|
| 157 |
+
"151662": {
|
| 158 |
+
"content": "<|fim_pad|>",
|
| 159 |
+
"lstrip": false,
|
| 160 |
+
"normalized": false,
|
| 161 |
+
"rstrip": false,
|
| 162 |
+
"single_word": false,
|
| 163 |
+
"special": false
|
| 164 |
+
},
|
| 165 |
+
"151663": {
|
| 166 |
+
"content": "<|repo_name|>",
|
| 167 |
+
"lstrip": false,
|
| 168 |
+
"normalized": false,
|
| 169 |
+
"rstrip": false,
|
| 170 |
+
"single_word": false,
|
| 171 |
+
"special": false
|
| 172 |
+
},
|
| 173 |
+
"151664": {
|
| 174 |
+
"content": "<|file_sep|>",
|
| 175 |
+
"lstrip": false,
|
| 176 |
+
"normalized": false,
|
| 177 |
+
"rstrip": false,
|
| 178 |
+
"single_word": false,
|
| 179 |
+
"special": false
|
| 180 |
+
},
|
| 181 |
+
"151665": {
|
| 182 |
+
"content": "<tool_response>",
|
| 183 |
+
"lstrip": false,
|
| 184 |
+
"normalized": false,
|
| 185 |
+
"rstrip": false,
|
| 186 |
+
"single_word": false,
|
| 187 |
+
"special": false
|
| 188 |
+
},
|
| 189 |
+
"151666": {
|
| 190 |
+
"content": "</tool_response>",
|
| 191 |
+
"lstrip": false,
|
| 192 |
+
"normalized": false,
|
| 193 |
+
"rstrip": false,
|
| 194 |
+
"single_word": false,
|
| 195 |
+
"special": false
|
| 196 |
+
},
|
| 197 |
+
"151667": {
|
| 198 |
+
"content": "<think>",
|
| 199 |
+
"lstrip": false,
|
| 200 |
+
"normalized": false,
|
| 201 |
+
"rstrip": false,
|
| 202 |
+
"single_word": false,
|
| 203 |
+
"special": false
|
| 204 |
+
},
|
| 205 |
+
"151668": {
|
| 206 |
+
"content": "</think>",
|
| 207 |
+
"lstrip": false,
|
| 208 |
+
"normalized": false,
|
| 209 |
+
"rstrip": false,
|
| 210 |
+
"single_word": false,
|
| 211 |
+
"special": false
|
| 212 |
+
}
|
| 213 |
+
},
|
| 214 |
+
"additional_special_tokens": [
|
| 215 |
+
"<|im_start|>",
|
| 216 |
+
"<|im_end|>",
|
| 217 |
+
"<|object_ref_start|>",
|
| 218 |
+
"<|object_ref_end|>",
|
| 219 |
+
"<|box_start|>",
|
| 220 |
+
"<|box_end|>",
|
| 221 |
+
"<|quad_start|>",
|
| 222 |
+
"<|quad_end|>",
|
| 223 |
+
"<|vision_start|>",
|
| 224 |
+
"<|vision_end|>",
|
| 225 |
+
"<|vision_pad|>",
|
| 226 |
+
"<|image_pad|>",
|
| 227 |
+
"<|video_pad|>",
|
| 228 |
+
"<d>",
|
| 229 |
+
"</d>",
|
| 230 |
+
"<|cutoff|>",
|
| 231 |
+
"<|lyrics_start|>",
|
| 232 |
+
"<|lyrics_end|>",
|
| 233 |
+
"<|caption_start|>",
|
| 234 |
+
"<|caption_end|>"
|
| 235 |
+
],
|
| 236 |
+
"bos_token": null,
|
| 237 |
+
"chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {%- if messages[0].content is string %}\n {{- messages[0].content }}\n {%- else %}\n {%- for content in messages[0].content %}\n {%- if 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].content is string %}\n {{- messages[0].content }}\n {%- else %}\n {%- for content in messages[0].content %}\n {%- if 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set image_count = namespace(value=0) %}\n{%- set video_count = namespace(value=0) %}\n{%- for message in messages %}\n {%- if message.role == \"user\" %}\n {{- '<|im_start|>' + message.role + '\\n' }}\n {%- if message.content is string %}\n {{- message.content }}\n {%- else %}\n {%- for content in message.content %}\n {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}\n {%- set image_count.value = image_count.value + 1 %}\n {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}\n <|vision_start|><|image_pad|><|vision_end|>\n {%- elif content.type == 'video' or 'video' in content %}\n {%- set video_count.value = video_count.value + 1 %}\n {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}\n <|vision_start|><|video_pad|><|vision_end|>\n {%- elif 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role + '\\n' }}\n {%- if message.content is string %}\n {{- message.content }}\n {%- else %}\n {%- for content_item in message.content %}\n {%- if 'text' in content_item %}\n {{- content_item.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and message.content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {%- if message.content is string %}\n {{- message.content }}\n {%- else %}\n {%- for content in message.content %}\n {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}\n {%- set image_count.value = image_count.value + 1 %}\n {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}\n <|vision_start|><|image_pad|><|vision_end|>\n {%- elif content.type == 'video' or 'video' in content %}\n {%- set video_count.value = video_count.value + 1 %}\n {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}\n <|vision_start|><|video_pad|><|vision_end|>\n {%- elif 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n",
|
| 238 |
+
"clean_up_tokenization_spaces": false,
|
| 239 |
+
"eos_token": "<|im_end|>",
|
| 240 |
+
"errors": "replace",
|
| 241 |
+
"model_max_length": 262144,
|
| 242 |
+
"pad_token": "<|endoftext|>",
|
| 243 |
+
"split_special_tokens": false,
|
| 244 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 245 |
+
"unk_token": null
|
| 246 |
+
}
|
custom_nodes/ComfyUI-MiniMaxH3-Adapter/tokenizer/vocab.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|