guychuk's picture
inference: Qwen3.5 support (VL unwrap, Gated-DeltaNet LoRA targets, linear_attn walk)
5924223 verified
Raw History Blame Contribute Delete
5.49 kB
"""Frozen backbone + LoRA adapter -- trimmed port of the training repo's encode.py, for
inference only. Only what native_kv_decide touches: tokenizer, .model (the raw HF causal
trunk, tap-truncated), LoRA weight loading. No FeatureCache/EmbedEncoder/forward() --
the native serving path reads hidden states straight off Backbone.model, never through a
Backbone.forward() (that method, and the frozen-feature/h_top machinery it supports, only
exists in the training repo's energy-head path). extra_tap is also dropped: both shipped
checkpoints (typical-small, typical-small-preview) train with extra_tap=0.
"""
import math
import torch
import torch.nn as nn
from transformers import AutoModel, AutoTokenizer
# Qwen3: q/k/v/o_proj (self_attn) + gate/up/down_proj (mlp). Qwen3.5 hybrid stack adds
# in_proj_{qkv,z,b,a}/out_proj for its Gated-DeltaNet linear-attention layers (linear_attn,
# no self_attn) -- listed here too since hasattr() gates each name per parent module, so this
# tuple is a superset that's a no-op for any module lacking a given name (Qwen3 untouched).
_LORA_MODULES = ("q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj",
"in_proj_qkv", "in_proj_z", "in_proj_b", "in_proj_a", "out_proj")
_printed_device = False
def pick_device(device: str = "auto") -> str:
global _printed_device
if device != "auto":
resolved = device
elif torch.cuda.is_available():
resolved = "cuda"
elif torch.backends.mps.is_available():
resolved = "mps"
else:
resolved = "cpu"
if not _printed_device:
print(f"typical: device={resolved}")
_printed_device = True
return resolved
class LoRALinear(nn.Module):
"""Frozen base nn.Linear (bf16) + a low-rank fp32 adapter on top. Numerics verbatim
from encode.py's LoRALinear (scale = alpha / r); dropout is a no-op once .eval()'d."""
def __init__(self, base: nn.Linear, r: int, alpha: float, dropout: float):
super().__init__()
self.base = base
self.base.requires_grad_(False)
self.A = nn.Parameter(torch.empty(r, base.in_features, dtype=torch.float32))
nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
self.B = nn.Parameter(torch.zeros(base.out_features, r, dtype=torch.float32))
self.drop = nn.Dropout(dropout)
self.scale = alpha / r
def forward(self, x):
out = self.base(x)
lora = self.drop(x) @ self.A.to(x.dtype).T @ self.B.to(x.dtype).T * self.scale
return out + lora
class Backbone(nn.Module):
"""Frozen causal-LM trunk (Qwen3-family), truncated to tap_layer, with LoRA on its top
lora_layers blocks. Base weights come fresh from `name`; the fine-tuned delta is only
ever the LoRA A/B tensors, loaded via load_lora_state_dict from the checkpoint."""
def __init__(self, name: str, lora_layers: int = 8, lora_r: int = 16, lora_alpha: float = 32,
lora_dropout: float = 0.05, device: str = "auto", tap_layer: int = 0):
super().__init__()
self.device = pick_device(device)
self.tokenizer = AutoTokenizer.from_pretrained(name, padding_side="right")
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
model = AutoModel.from_pretrained(name, dtype=torch.bfloat16, attn_implementation="sdpa")
# Qwen3.5 family: AutoModel resolves to the VL wrapper (.visual + .language_model) even for
# a text-only checkpoint -- see encode.py's Backbone for the long form. Take the text trunk.
if hasattr(model, "language_model") and hasattr(model, "visual"):
model = model.language_model
self.model = model
self.model.requires_grad_(False)
self.d = self.model.config.hidden_size
n_layers = self.model.config.num_hidden_layers
# tap_layer: drop every layer above T (semantics live mid-depth, not at the final,
# next-token-shaped layer of a base LM) -- see encode.py's Backbone for the long form.
if 0 < tap_layer < n_layers:
self.model.layers = self.model.layers[:tap_layer]
self.model.config.num_hidden_layers = tap_layer
n_layers = tap_layer
self.info = {"n_layers": n_layers,
"layer_types": list(getattr(self.model.config, "layer_types", None)
or ["full_attention"] * n_layers)[:n_layers]}
if lora_r > 0:
for layer in self.model.layers[-lora_layers:]:
# Qwen3.5 linear_attention layers have linear_attn (Gated DeltaNet) instead of
# self_attn; mlp is present on every layer either way -- see encode.py's Backbone.
parents = [p for p in (getattr(layer, "self_attn", None), getattr(layer, "linear_attn", None),
getattr(layer, "mlp", None)) if p is not None]
for parent in parents:
for mod_name in _LORA_MODULES:
if hasattr(parent, mod_name):
setattr(parent, mod_name,
LoRALinear(getattr(parent, mod_name), lora_r, lora_alpha, lora_dropout))
self.to(self.device)
self.eval()
def load_lora_state_dict(self, sd: dict):
own = dict(self.named_parameters())
for k, v in sd.items():
own[k].data.copy_(v.to(own[k].device, own[k].dtype))