laya-ad / code /laya-server /backend /server /quant_loader.py
x6nux's picture
laya-ad v2.2: sync laya-server code (default version v2.2, build_push.sh, .env.example)
0cd599e verified
Raw History Blame Contribute Delete
5.65 kB
"""量化变体加载器(laya-ad 发布格式)。
支持 laya-ad Release 的五种 safetensors 变体并统一加载为可推理模型:
- fp16 / bf16:transformers 原生加载
- fp8(float8_e4m3fn 存储):cast 回 bf16
- int8(per-channel 对称 weight-only,`__scale__` 伴生 fp16)
- int4(group=64 对称、双 nibble 打包 uint8、`__scale__`/`__pad__` 伴生)
由 scripts/quantize_variants.py 产出;格式与 laya-ad 仓库 scripts/load_quantized.py 一致。
"""
import json
from pathlib import Path
from types import SimpleNamespace
import torch
from safetensors import safe_open
INT4_GROUP = 64
class _DualHeadBinary(torch.nn.Module):
"""把多任务模型包装成服务端可用的模型:binary/kind/scope/severity 四组 logits。"""
def __init__(self, model, kind_labels: list[str] | None = None,
scope_labels: list[str] | None = None,
severity_labels: list[str] | None = None):
super().__init__()
self.model = model
self.config = model.config
self.kind_labels = list(kind_labels or [])
self.scope_labels = list(scope_labels or [])
self.severity_labels = list(severity_labels or [])
self.has_scope = bool(self.scope_labels) and getattr(model, "scope_head", None) is not None
self.has_severity = (bool(self.severity_labels)
and getattr(model, "severity_head", None) is not None)
def forward(self, input_ids=None, attention_mask=None, **kwargs):
binary_logits, kind_logits, scope_logits, severity_logits, _ = self.model.forward_all(
input_ids, attention_mask)
return SimpleNamespace(logits=binary_logits, kind_logits=kind_logits,
scope_logits=scope_logits, severity_logits=severity_logits)
def detect_precision(model_dir: Path) -> str:
"""优先读 PRECISION.txt(CI 注入),否则从权重 dtype 推断。"""
marker = Path(model_dir) / "PRECISION.txt"
if marker.is_file():
return marker.read_text(encoding="utf-8").strip() or "unknown"
sd = Path(model_dir) / "model.safetensors"
dts = set()
with safe_open(str(sd), framework="pt") as f:
for k in f.keys():
dts.add(f.get_slice(k).get_dtype())
if "F8_E4M3" in dts:
return "fp8"
if "U8" in dts:
return "int4"
if "I8" in dts:
return "int8"
if "BF16" in dts:
return "bf16"
if "F16" in dts:
return "fp16"
return "fp32"
def load_ad_model(model_dir, device: str = "cpu", dtype: torch.dtype = torch.bfloat16):
"""加载广告分类模型(自动识别量化格式),返回 (model, tokenizer),eval 模式。"""
from transformers import AutoConfig, AutoModelForSequenceClassification, AutoTokenizer
model_dir = Path(model_dir)
if (model_dir / "dual_config.json").is_file():
# v2 双头模型:共享 encoder + binary/kind 两个头
from transformers import AutoTokenizer
from .dual_head_model import load_dual_head_any
dual_cfg = json.loads((model_dir / "dual_config.json").read_text(encoding="utf-8"))
kind_labels = [str(x) for x in dual_cfg.get("kind_labels") or []]
scope_labels = [str(x) for x in dual_cfg.get("scope_labels") or []]
severity_labels = [str(x) for x in dual_cfg.get("severity_labels") or []]
dual = load_dual_head_any(str(model_dir), device=device, dtype=dtype)
return (_DualHeadBinary(dual, kind_labels, scope_labels, severity_labels).to(device).eval(),
AutoTokenizer.from_pretrained(str(model_dir)))
sd_path = model_dir / "model.safetensors"
dts = set()
with safe_open(str(sd_path), framework="pt") as f:
for k in f.keys():
dts.add(f.get_slice(k).get_dtype())
if not ({"I8", "U8", "F8_E4M3"} & dts):
kw = {"dtype": dtype}
model = AutoModelForSequenceClassification.from_pretrained(
str(model_dir), attn_implementation="sdpa", **kw)
tok = AutoTokenizer.from_pretrained(str(model_dir))
return model.to(device).eval(), tok
model = AutoModelForSequenceClassification.from_config(
AutoConfig.from_pretrained(str(model_dir)))
param_shapes = {k: v.shape for k, v in model.state_dict().items()}
with safe_open(str(sd_path), framework="pt") as f:
raw = {k: f.get_tensor(k) for k in f.keys()}
state = {}
for k, v in raw.items():
if k.endswith(".__scale__") or k.endswith(".__pad__"):
continue
if v.dtype == torch.float8_e4m3fn:
state[k] = v.to(dtype)
elif v.dtype == torch.int8 and k + ".__scale__" in raw:
scale = raw[k + ".__scale__"].float().view(-1, *([1] * (v.dim() - 1)))
state[k] = (v.float() * scale).to(dtype)
elif v.dtype == torch.uint8 and k + ".__scale__" in raw:
hi = (v >> 4).to(torch.int16) - 8
lo = (v & 0xF).to(torch.int16) - 8
q = torch.stack([hi, lo], dim=-1).reshape(-1).float()
scale = raw[k + ".__scale__"].float()
s_exp = scale.repeat_interleave(INT4_GROUP)[:q.numel()]
w = q * s_exp
pad = int(raw[k + ".__pad__"].item()) if k + ".__pad__" in raw else 0
if pad:
w = w[:-pad]
state[k] = w.reshape(param_shapes[k]).to(dtype)
else: # 未量化透传(1D bias/norm 等保留精度)
state[k] = v.to(dtype)
model.load_state_dict(state, strict=True)
tok = AutoTokenizer.from_pretrained(str(model_dir))
return model.to(device).eval(), tok