ajh-code commited on
Commit
de52d57
·
verified ·
1 Parent(s): 48b8427

Add runtime/quant_text_encoder.py

Browse files
Files changed (1) hide show
  1. runtime/quant_text_encoder.py +376 -0
runtime/quant_text_encoder.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load the packaged mixed NVFP4/FP8 Qwen3-VL text encoder without BF16 shards."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+ from accelerate import init_empty_weights
14
+ from accelerate.utils import set_module_tensor_to_device
15
+ from safetensors import safe_open
16
+ from transformers import AutoConfig, AutoProcessor, AutoTokenizer
17
+
18
+
19
+ EXPECTED_ARTIFACT_SHA256 = (
20
+ "719906b435800757d22013d3d475a4853d59b779b022669fa8b8a193b85d0f41"
21
+ )
22
+ EXPECTED_FORMAT_COUNTS = {"nvfp4": 224, "float8_e4m3fn": 14}
23
+ EXPECTED_PROJECTIONS = {
24
+ "mlp.down_proj",
25
+ "mlp.gate_proj",
26
+ "mlp.up_proj",
27
+ "self_attn.k_proj",
28
+ "self_attn.o_proj",
29
+ "self_attn.q_proj",
30
+ "self_attn.v_proj",
31
+ }
32
+
33
+
34
+ def sha256(path: Path) -> str:
35
+ digest = hashlib.sha256()
36
+ with path.open("rb") as handle:
37
+ for chunk in iter(lambda: handle.read(1 << 20), b""):
38
+ digest.update(chunk)
39
+ return digest.hexdigest()
40
+
41
+
42
+ def _decode_quant_config(tensor: torch.Tensor) -> dict[str, Any]:
43
+ payload = bytes(tensor.cpu().to(torch.uint8).tolist()).decode("utf-8")
44
+ parsed = json.loads(payload)
45
+ if not isinstance(parsed, dict) or not isinstance(parsed.get("format"), str):
46
+ raise ValueError(f"invalid comfy_quant payload: {payload!r}")
47
+ return parsed
48
+
49
+
50
+ def _artifact_layer_to_hf_module(layer_key: str) -> str:
51
+ prefix = "model.layers."
52
+ if not layer_key.startswith(prefix):
53
+ raise ValueError(f"quantized layer is outside the language stack: {layer_key}")
54
+ remainder = layer_key[len(prefix) :]
55
+ layer_text, projection = remainder.split(".", 1)
56
+ layer_index = int(layer_text)
57
+ if layer_index < 0 or layer_index >= 36:
58
+ raise ValueError(f"language layer index is out of range: {layer_key}")
59
+ if projection not in EXPECTED_PROJECTIONS:
60
+ raise ValueError(f"unexpected quantized projection: {layer_key}")
61
+ return f"model.language_model.layers.{layer_index}.{projection}"
62
+
63
+
64
+ def _artifact_weight_to_hf_name(key: str) -> str | None:
65
+ if key == "model.embed_tokens.weight":
66
+ return "model.language_model.embed_tokens.weight"
67
+ if key == "model.norm.weight":
68
+ return "model.language_model.norm.weight"
69
+ if key.startswith("model.layers."):
70
+ return "model.language_model.layers." + key.removeprefix("model.layers.")
71
+ if key.startswith("model.visual."):
72
+ return key
73
+ return None
74
+
75
+
76
+ def _resolve_parent(module: torch.nn.Module, dotted_name: str) -> tuple[Any, str]:
77
+ parts = dotted_name.split(".")
78
+ parent: Any = module
79
+ for part in parts[:-1]:
80
+ parent = getattr(parent, part)
81
+ return parent, parts[-1]
82
+
83
+
84
+ class PublishedQuantLinear(torch.nn.Module):
85
+ """Inference-only projection backed by comfy-kitchen packed tensors."""
86
+
87
+ def __init__(
88
+ self,
89
+ *,
90
+ in_features: int,
91
+ out_features: int,
92
+ quant_format: str,
93
+ qdata: torch.Tensor,
94
+ weight_scale: torch.Tensor,
95
+ weight_scale_2: torch.Tensor | None,
96
+ ) -> None:
97
+ super().__init__()
98
+ self.in_features = int(in_features)
99
+ self.out_features = int(out_features)
100
+ self.quant_format = str(quant_format)
101
+ self.register_buffer("qdata", qdata.clone().contiguous(), persistent=True)
102
+ self.register_buffer(
103
+ "weight_scale",
104
+ weight_scale.clone().contiguous(),
105
+ persistent=True,
106
+ )
107
+ if weight_scale_2 is None:
108
+ self.weight_scale_2 = None
109
+ else:
110
+ self.register_buffer(
111
+ "weight_scale_2",
112
+ weight_scale_2.clone().contiguous(),
113
+ persistent=True,
114
+ )
115
+
116
+ def _weight_quantized_tensor(self) -> Any:
117
+ from comfy_kitchen.tensor import (
118
+ QuantizedTensor,
119
+ TensorCoreFP8Layout,
120
+ TensorCoreNVFP4Layout,
121
+ )
122
+
123
+ shape = (self.out_features, self.in_features)
124
+ if self.quant_format == "nvfp4":
125
+ if self.weight_scale_2 is None:
126
+ raise RuntimeError("NVFP4 projection is missing its second scale")
127
+ params = TensorCoreNVFP4Layout.Params(
128
+ scale=self.weight_scale_2,
129
+ orig_dtype=torch.bfloat16,
130
+ orig_shape=shape,
131
+ block_scale=self.weight_scale,
132
+ )
133
+ return QuantizedTensor(
134
+ self.qdata,
135
+ "TensorCoreNVFP4Layout",
136
+ params,
137
+ )
138
+ if self.quant_format == "float8_e4m3fn":
139
+ params = TensorCoreFP8Layout.Params(
140
+ scale=self.weight_scale,
141
+ orig_dtype=torch.bfloat16,
142
+ orig_shape=shape,
143
+ )
144
+ return QuantizedTensor(
145
+ self.qdata,
146
+ "TensorCoreFP8Layout",
147
+ params,
148
+ )
149
+ raise ValueError(f"unsupported quantized format: {self.quant_format}")
150
+
151
+ def forward(self, value: torch.Tensor) -> torch.Tensor:
152
+ from comfy_kitchen.tensor import QuantizedTensor
153
+
154
+ input_shape = tuple(value.shape)
155
+ flattened = value.reshape(-1, input_shape[-1]).contiguous()
156
+ layout = (
157
+ "TensorCoreNVFP4Layout"
158
+ if self.quant_format == "nvfp4"
159
+ else "TensorCoreFP8Layout"
160
+ )
161
+ input_quantized = QuantizedTensor.from_float(flattened, layout)
162
+ output = F.linear(
163
+ input_quantized,
164
+ self._weight_quantized_tensor(),
165
+ None,
166
+ )
167
+ return output.reshape(*input_shape[:-1], self.out_features)
168
+
169
+
170
+ def _install_quantized_linears(
171
+ hf_module: torch.nn.Module,
172
+ artifact_path: Path,
173
+ ) -> dict[str, Any]:
174
+ format_counts: Counter[str] = Counter()
175
+ installed: list[str] = []
176
+ storage_bytes = 0
177
+ original_bf16_bytes = 0
178
+
179
+ with safe_open(str(artifact_path), framework="pt", device="cpu") as handle:
180
+ config_keys = sorted(
181
+ key for key in handle.keys() if key.endswith(".comfy_quant")
182
+ )
183
+ for config_key in config_keys:
184
+ layer_key = config_key.removesuffix(".comfy_quant")
185
+ quant_format = _decode_quant_config(
186
+ handle.get_tensor(config_key)
187
+ )["format"]
188
+ module_name = _artifact_layer_to_hf_module(layer_key)
189
+ parent, leaf = _resolve_parent(hf_module, module_name)
190
+ original = getattr(parent, leaf)
191
+ if not isinstance(original, torch.nn.Linear):
192
+ raise TypeError(
193
+ f"{module_name}: expected torch.nn.Linear, got "
194
+ f"{type(original).__name__}"
195
+ )
196
+ if original.bias is not None:
197
+ raise ValueError(f"{module_name}: quantized projection has a bias")
198
+
199
+ qdata = handle.get_tensor(f"{layer_key}.weight")
200
+ weight_scale = handle.get_tensor(f"{layer_key}.weight_scale")
201
+ weight_scale_2 = (
202
+ handle.get_tensor(f"{layer_key}.weight_scale_2")
203
+ if quant_format == "nvfp4"
204
+ else None
205
+ )
206
+ replacement = PublishedQuantLinear(
207
+ in_features=original.in_features,
208
+ out_features=original.out_features,
209
+ quant_format=quant_format,
210
+ qdata=qdata,
211
+ weight_scale=weight_scale,
212
+ weight_scale_2=weight_scale_2,
213
+ )
214
+ setattr(parent, leaf, replacement)
215
+ format_counts[quant_format] += 1
216
+ installed.append(module_name)
217
+ original_bf16_bytes += (
218
+ original.in_features * original.out_features * 2
219
+ )
220
+ storage_bytes += replacement.qdata.nbytes
221
+ storage_bytes += replacement.weight_scale.nbytes
222
+ if replacement.weight_scale_2 is not None:
223
+ storage_bytes += replacement.weight_scale_2.nbytes
224
+
225
+ summary = {
226
+ "installed_module_count": len(installed),
227
+ "format_counts": dict(format_counts),
228
+ "original_projection_bf16_bytes": original_bf16_bytes,
229
+ "packed_projection_bytes": storage_bytes,
230
+ "projection_saving_bytes": original_bf16_bytes - storage_bytes,
231
+ "projection_saving_gib": (
232
+ (original_bf16_bytes - storage_bytes) / float(1 << 30)
233
+ ),
234
+ }
235
+ if (
236
+ summary["installed_module_count"] != 238
237
+ or summary["format_counts"] != EXPECTED_FORMAT_COUNTS
238
+ ):
239
+ raise RuntimeError(f"unexpected text quantization policy: {summary}")
240
+ return summary
241
+
242
+
243
+ def _load_nonquantized_weights(
244
+ hf_module: torch.nn.Module,
245
+ artifact_path: Path,
246
+ ) -> dict[str, Any]:
247
+ loaded: list[str] = []
248
+ unexpected: list[str] = []
249
+
250
+ with safe_open(str(artifact_path), framework="pt", device="cpu") as handle:
251
+ for artifact_key in sorted(handle.keys()):
252
+ if (
253
+ artifact_key.endswith(".comfy_quant")
254
+ or artifact_key.endswith(".weight_scale")
255
+ or artifact_key.endswith(".weight_scale_2")
256
+ ):
257
+ continue
258
+ target_name = _artifact_weight_to_hf_name(artifact_key)
259
+ if target_name is None:
260
+ unexpected.append(artifact_key)
261
+ continue
262
+ try:
263
+ parent, leaf = _resolve_parent(hf_module, target_name)
264
+ except AttributeError:
265
+ unexpected.append(artifact_key)
266
+ continue
267
+ current = getattr(parent, leaf, None)
268
+ if isinstance(current, PublishedQuantLinear):
269
+ continue
270
+ if target_name.endswith(".weight"):
271
+ projection_name = target_name.removesuffix(".weight")
272
+ try:
273
+ projection_parent, projection_leaf = _resolve_parent(
274
+ hf_module, projection_name
275
+ )
276
+ if isinstance(
277
+ getattr(projection_parent, projection_leaf),
278
+ PublishedQuantLinear,
279
+ ):
280
+ continue
281
+ except AttributeError:
282
+ pass
283
+ set_module_tensor_to_device(
284
+ hf_module,
285
+ target_name,
286
+ "cpu",
287
+ value=handle.get_tensor(artifact_key),
288
+ )
289
+ loaded.append(target_name)
290
+
291
+ hf_module.tie_weights()
292
+ meta_parameters = [
293
+ name for name, value in hf_module.named_parameters() if value.is_meta
294
+ ]
295
+ meta_buffers = [
296
+ name for name, value in hf_module.named_buffers() if value.is_meta
297
+ ]
298
+ if meta_parameters or meta_buffers:
299
+ raise RuntimeError(
300
+ "packed text loader left unresolved meta tensors: "
301
+ f"{(meta_parameters + meta_buffers)[:4]}"
302
+ )
303
+ if unexpected:
304
+ raise RuntimeError(
305
+ f"packed text artifact contains unmapped tensors: {unexpected[:4]}"
306
+ )
307
+ return {
308
+ "loaded_nonquantized_tensor_count": len(loaded),
309
+ "unresolved_meta_parameters": meta_parameters,
310
+ "unresolved_meta_buffers": meta_buffers,
311
+ }
312
+
313
+
314
+ def load_quantized_text_encoder(
315
+ *,
316
+ text_encoder_dir: str | Path,
317
+ artifact_path: str | Path,
318
+ tokenizer_max_length: int,
319
+ dit_structure: dict[str, Any],
320
+ use_packed_text_infer: bool,
321
+ ) -> tuple[torch.nn.Module, dict[str, Any]]:
322
+ """Construct Mage's text wrapper directly from the packaged quant artifact."""
323
+
324
+ from mage_flow.models.modules.text_encoder import (
325
+ CustomQwen3VLForConditionalGeneration,
326
+ TextEncoder,
327
+ )
328
+
329
+ text_encoder_dir = Path(text_encoder_dir).resolve()
330
+ artifact_path = Path(artifact_path).resolve()
331
+ if sha256(artifact_path) != EXPECTED_ARTIFACT_SHA256:
332
+ raise RuntimeError("packaged text-encoder artifact SHA-256 mismatch")
333
+
334
+ config = AutoConfig.from_pretrained(
335
+ str(text_encoder_dir),
336
+ local_files_only=True,
337
+ )
338
+ with init_empty_weights():
339
+ hf_module = CustomQwen3VLForConditionalGeneration._from_config(
340
+ config,
341
+ attn_implementation="flash_attention_2",
342
+ dtype=torch.bfloat16,
343
+ )
344
+
345
+ quant_summary = _install_quantized_linears(hf_module, artifact_path)
346
+ load_summary = _load_nonquantized_weights(hf_module, artifact_path)
347
+
348
+ text_encoder = TextEncoder.__new__(TextEncoder)
349
+ torch.nn.Module.__init__(text_encoder)
350
+ text_encoder.model_name = str(text_encoder_dir)
351
+ text_encoder.tokenizer_max_length = int(tokenizer_max_length)
352
+ text_encoder.tokenizer = AutoTokenizer.from_pretrained(
353
+ str(text_encoder_dir),
354
+ local_files_only=True,
355
+ )
356
+ text_encoder.tokenizer.padding_side = "right"
357
+ text_encoder.processor = AutoProcessor.from_pretrained(
358
+ str(text_encoder_dir),
359
+ local_files_only=True,
360
+ )
361
+ text_encoder.hf_module = hf_module.eval().requires_grad_(False)
362
+ text_encoder.prompt_template_encode = ""
363
+ text_encoder.prompt_template_encode_start_idx = 0
364
+ text_encoder.dit_structure = dict(dit_structure)
365
+ text_encoder.use_packed_text_infer = bool(use_packed_text_infer)
366
+ text_encoder.eval().requires_grad_(False)
367
+
368
+ return (
369
+ text_encoder,
370
+ {
371
+ "artifact": str(artifact_path),
372
+ "artifact_sha256": EXPECTED_ARTIFACT_SHA256,
373
+ "quantized": quant_summary,
374
+ "nonquantized": load_summary,
375
+ },
376
+ )