Download modeling_projector.py from internlm/Sage-SciPerception: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/internlm/Sage-SciPerception/resolve/main/modeling_projector.py
- Command line
-
hf download hf://internlm/Sage-SciPerception/modeling_projector.py
-
curl -L -o modeling_projector.py https://huggingface.co/internlm/Sage-SciPerception/resolve/main/modeling_projector.py
16.7 kB
| """Shared MLP with learned slot identities; state keys match the exported weights.""" | |
| import math | |
| import torch | |
| from torch import nn | |
| class OutputNorm(nn.Module): | |
| """Parameter-free RMSNorm then one learned scale, kept in log space so decay pulls toward 1.""" | |
| def __init__(self, scale, eps=1e-12): | |
| super().__init__() | |
| self.eps = eps | |
| self.log_scale = nn.Parameter(torch.tensor(math.log(scale))) | |
| def forward(self, values): | |
| # Initial outputs have RMS ~1e-2 (last-layer std 0.001), so a usual 1e-6 eps would shrink them. | |
| values = values * torch.rsqrt(values.pow(2).mean(-1, keepdim=True) + self.eps) | |
| # RMS norm of a d-dim vector is sqrt(d), so divide to make `scale` the target L2 norm. | |
| return values * (self.log_scale.exp() / math.sqrt(values.shape[-1])) | |
| class ResidualBlock(nn.Module): | |
| """Pre-LN MLP block; the zero-initialized exit makes extra depth start as the identity.""" | |
| def __init__(self, hidden_dim, mult): | |
| super().__init__() | |
| self.norm = nn.LayerNorm(hidden_dim) | |
| self.up = nn.Linear(hidden_dim, hidden_dim * mult) | |
| self.down = nn.Linear(hidden_dim * mult, hidden_dim) | |
| nn.init.zeros_(self.down.weight) | |
| nn.init.zeros_(self.down.bias) | |
| def forward(self, values): | |
| return values + self.down(nn.functional.gelu(self.up(self.norm(values)))) | |
| class ContextResidual(nn.Module): | |
| """A zero-exit global readout within each scientific object, before translation to GLM.""" | |
| def __init__(self, hidden_dim, model_dim, layers, heads): | |
| super().__init__() | |
| self.input = nn.Sequential(nn.LayerNorm(hidden_dim), nn.Linear(hidden_dim, model_dim)) | |
| layer = nn.TransformerEncoderLayer(model_dim, heads, model_dim * 2, dropout=0.0, | |
| activation='gelu', batch_first=True, norm_first=True) | |
| self.encoder = nn.TransformerEncoder(layer, layers, norm=nn.LayerNorm(model_dim), | |
| enable_nested_tensor=False) | |
| self.output = nn.Linear(model_dim, hidden_dim) | |
| nn.init.zeros_(self.output.weight) | |
| nn.init.zeros_(self.output.bias) | |
| def forward(self, hidden, counts): | |
| if not len(hidden): | |
| return hidden + sum(parameter.sum() * 0 for parameter in self.parameters()) | |
| values = self.input(hidden) | |
| chunks = values.split(counts.tolist()) | |
| padded = nn.utils.rnn.pad_sequence(chunks, batch_first=True) | |
| valid = torch.arange(padded.shape[1], device=hidden.device)[None, :] < counts[:, None] | |
| encoded = self.encoder(padded, src_key_padding_mask=~valid) | |
| return hidden + self.output(encoded[valid]) | |
| class ClassificationReadout(nn.Module): | |
| """A pretrained scientific classifier followed by a trainable translation to GLM space.""" | |
| def __init__(self, feature_dim, output_dim, classes, side_dim=0): | |
| super().__init__() | |
| self.per_atom = classes == 1 # A single output is the per-residue binary task. | |
| self.side_dim = side_dim | |
| width = (feature_dim + side_dim) * (1 if self.per_atom else 5) + (0 if self.per_atom else 1) | |
| self.register_buffer('feature_mean', torch.zeros(width)) | |
| self.register_buffer('feature_std', torch.ones(width)) | |
| self.classifier = (nn.Sequential(nn.LayerNorm(width),nn.Linear(width,256),nn.GELU(),nn.Linear(256,1)) | |
| if self.per_atom else nn.Sequential(nn.Linear(width,256),nn.GELU(),nn.Dropout(0.),nn.Linear(256,classes))) | |
| self.bridge = nn.Sequential(nn.Linear(classes,128),nn.GELU(),nn.Linear(128,output_dim)) | |
| nn.init.zeros_(self.bridge[-1].weight) | |
| nn.init.zeros_(self.bridge[-1].bias) | |
| self.classes = classes | |
| def probabilities(self, values, counts, side=None): | |
| if not len(values): | |
| return values.new_empty((0,self.classes)) | |
| if self.side_dim: | |
| values = torch.cat([values,side.float()],dim=-1) | |
| if self.per_atom: | |
| normalized = (values-self.feature_mean)/self.feature_std | |
| return self.classifier(normalized).sigmoid() | |
| pooled = [] | |
| for item in values.split(counts.tolist()): | |
| if self.side_dim: | |
| # The lightcurve teacher pools joint states and measurements at floor-spaced edges. | |
| edges = [i*len(item)//4 for i in range(5)] | |
| regions = [item[left:right].mean(0) if right>left else item[min(left,len(item)-1)] | |
| for left,right in zip(edges[:-1],edges[1:])] | |
| else: | |
| regions = [chunk.mean(0) if len(chunk) else item[-1] for chunk in item.tensor_split(4)] | |
| pooled.append(torch.cat([item.mean(0),*regions,item.new_tensor([math.log1p(len(item))])])) | |
| normalized = (torch.stack(pooled)-self.feature_mean)/self.feature_std | |
| return self.classifier(normalized).softmax(-1) | |
| def forward(self, values, counts, side=None): | |
| translated = self.bridge(self.probabilities(values,counts,side)) | |
| return translated if self.per_atom else translated.repeat_interleave(counts,dim=0) | |
| class BondResidualBlock(nn.Module): | |
| """Mean bond messages; a zero exit initially preserves the frozen node states.""" | |
| def __init__(self, feature_dim, hidden_dim): | |
| super().__init__() | |
| self.norm = nn.LayerNorm(feature_dim) | |
| self.up = nn.Linear(feature_dim, hidden_dim) | |
| self.bond = nn.Embedding(4, hidden_dim) | |
| self.down = nn.Linear(hidden_dim, feature_dim) | |
| nn.init.normal_(self.bond.weight, std=0.02) | |
| nn.init.zeros_(self.down.weight) | |
| nn.init.zeros_(self.down.bias) | |
| def forward(self, values, edge_index, bond_types): | |
| source, target = edge_index | |
| messages = nn.functional.gelu(self.up(self.norm(values)[source]) + self.bond(bond_types)) | |
| aggregate = values.new_zeros((len(values), messages.shape[-1])).index_add_(0, target, messages) | |
| degree = values.new_zeros(len(values)).index_add_(0, target, values.new_ones(len(target))) | |
| return values + self.down(aggregate / degree.clamp_min(1)[:, None]) | |
| class LeadProjector(nn.Module): | |
| def __init__(self, feature_dim, tokens, hidden_dim, output_dim, output_scale=None): | |
| super().__init__() | |
| self.payload_tokens = tokens | |
| self.feature_dim = feature_dim | |
| self.output_dim = output_dim | |
| self.identity = nn.Parameter(torch.empty(tokens, feature_dim)) | |
| nn.init.normal_(self.identity, std=0.02) | |
| self.layers = nn.Sequential(nn.LayerNorm(feature_dim), nn.Linear(feature_dim, hidden_dim), | |
| nn.GELU(), nn.Linear(hidden_dim, output_dim)) | |
| nn.init.normal_(self.layers[-1].weight, std=0.001) | |
| nn.init.zeros_(self.layers[-1].bias) | |
| self.output_norm = OutputNorm(output_scale) if output_scale else nn.Identity() | |
| def forward(self, values): | |
| return self.output_norm(self.layers(values.float() + self.identity)) | |
| class AtomProjector(nn.Module): | |
| def __init__(self, feature_dim, index_dim, max_atoms, hidden_dim, output_dim, output_scale=None, | |
| discrete_sizes=(), discrete_dim=0, depth=0, block_mult=2, block_exit_norm=False, | |
| graph_layers=0, graph_hidden_dim=128, side_dim=0, side_hidden_dim=128, | |
| context_layers=0, context_dim=256, context_heads=8, readout_classes=0, | |
| readout_uses_side=True): | |
| super().__init__() | |
| self.graph = nn.ModuleList(BondResidualBlock(feature_dim, graph_hidden_dim) for _ in range(graph_layers)) | |
| self.needs_graph = bool(graph_layers) | |
| self.atom_index = nn.Embedding(max_atoms, index_dim) | |
| # The frozen GIN mixes its neighbourhood into every state; these embeddings give the | |
| # projector each atom's own discrete properties unmixed, in the exported column order. | |
| self.discrete = nn.ModuleList([nn.Embedding(size, discrete_dim) for size in discrete_sizes]) | |
| width = feature_dim + index_dim + discrete_dim * len(self.discrete) | |
| # depth 0 keeps the original four-element Sequential, so earlier state keys still load. | |
| blocks = [ResidualBlock(hidden_dim, block_mult) for _ in range(depth)] | |
| # A pre-LN residual stack normalizes each block's input but never its own output, so the | |
| # stream reaches the exit projection unbounded: e1-stm-num-deep (depth 2, no exit norm) | |
| # trained to payload norms 250,000x the vocabulary rows and scored near zero. Keep the | |
| # flag so that run stays reproducible from its manifest; set it on every new deep arm. | |
| exit_norm = [nn.LayerNorm(hidden_dim)] if depth and block_exit_norm else [] | |
| self.layers = nn.Sequential(nn.LayerNorm(width), nn.Linear(width, hidden_dim), nn.GELU(), | |
| *blocks, *exit_norm, nn.Linear(hidden_dim, output_dim)) | |
| for embedding in (self.atom_index, *self.discrete): | |
| nn.init.normal_(embedding.weight, std=0.02) | |
| nn.init.normal_(self.layers[-1].weight, std=0.001) | |
| nn.init.zeros_(self.layers[-1].bias) | |
| self.needs_context = bool(context_layers) | |
| self.needs_counts = bool(context_layers or readout_classes > 1) | |
| if context_layers: | |
| self.context = ContextResidual(hidden_dim, context_dim, context_layers, context_heads) | |
| self.output_norm = OutputNorm(output_scale) if output_scale else nn.Identity() | |
| self.needs_side = bool(side_dim) | |
| if side_dim: | |
| self.side = nn.Sequential(nn.Linear(side_dim, side_hidden_dim, bias=False), nn.GELU(), | |
| nn.Linear(side_hidden_dim, output_dim, bias=False)) | |
| nn.init.zeros_(self.side[-1].weight) | |
| if readout_classes: | |
| self.readout = ClassificationReadout(feature_dim,output_dim,readout_classes, | |
| side_dim if readout_uses_side else 0) | |
| def forward(self, values, atom_indices, discrete=None, *, edge_index=None, bond_types=None, side=None, counts=None): | |
| values = values.float() | |
| if self.needs_graph: | |
| if edge_index is None or bond_types is None: | |
| raise ValueError('bond residual projector requires original graph edges and bond types') | |
| for block in self.graph: | |
| values = block(values, edge_index, bond_types) | |
| parts = [values, self.atom_index(atom_indices)] | |
| if len(self.discrete) != (0 if discrete is None else discrete.shape[-1]): | |
| raise ValueError('discrete atom feature columns and embeddings disagree') | |
| parts += [embedding(discrete[:, column]) for column, embedding in enumerate(self.discrete)] | |
| hidden = self.layers[:-1](torch.cat(parts, dim=-1)) | |
| if self.needs_context: | |
| hidden = self.context(hidden, counts) | |
| output = self.layers[-1](hidden) | |
| if self.needs_side: | |
| output = output + self.side(side.float()) | |
| if hasattr(self,'readout'): | |
| output = output + self.readout(values,counts,side) | |
| return self.output_norm(output) | |
| class AtomSetProjector(nn.Module): | |
| """Atoms of one molecule attend to each other; no graph structure, no cross-molecule leak. | |
| The flat per-atom batch is regrouped into `[molecules, longest, model_dim]` with a key | |
| padding mask, so a molecule's output cannot depend on what else shares the batch. | |
| """ | |
| needs_counts = True | |
| def __init__(self, feature_dim, index_dim, max_atoms, model_dim, layers, heads, ffn_dim, | |
| output_dim, output_scale=None): | |
| super().__init__() | |
| self.atom_index = nn.Embedding(max_atoms, index_dim) | |
| nn.init.normal_(self.atom_index.weight, std=0.02) | |
| width = feature_dim + index_dim | |
| self.input = nn.Sequential(nn.LayerNorm(width), nn.Linear(width, model_dim)) | |
| layer = nn.TransformerEncoderLayer(model_dim, heads, ffn_dim, dropout=0.0, | |
| activation='gelu', batch_first=True, norm_first=True) | |
| self.encoder = nn.TransformerEncoder(layer, layers, norm=nn.LayerNorm(model_dim), | |
| enable_nested_tensor=False) | |
| self.output = nn.Linear(model_dim, output_dim) | |
| nn.init.normal_(self.output.weight, std=0.001) | |
| nn.init.zeros_(self.output.bias) | |
| self.output_norm = OutputNorm(output_scale) if output_scale else nn.Identity() | |
| def forward(self, values, atom_indices, counts): | |
| hidden = self.input(torch.cat([values.float(), self.atom_index(atom_indices)], dim=-1)) | |
| if hidden.shape[0] == 0: | |
| return self.output_norm(self.output(hidden)) | |
| # Wrong boundaries would mix molecules without ever failing, so check them here. | |
| if int(counts.sum()) != hidden.shape[0] or int(counts.min()) < 1: | |
| raise ValueError('molecule atom counts do not partition the payload rows') | |
| longest = int(counts.max()) | |
| valid = torch.arange(longest, device=hidden.device)[None, :] < counts.to(hidden.device)[:, None] | |
| padded = hidden.new_zeros((len(counts), longest, hidden.shape[-1])) | |
| padded[valid] = hidden | |
| encoded = self.encoder(padded, src_key_padding_mask=~valid) | |
| return self.output_norm(self.output(encoded[valid])) | |
| def build(spec, output_dim, *, train_core=False, train_side_only=False): | |
| # output_scale is the initial L2 norm of each payload vector; absent keeps the old unnormalized output. | |
| scale = spec.get("output_scale") | |
| if spec.get('graph_layers') and spec.get('kind') != 'atom': | |
| raise ValueError('bond residual layers require the atom projector') | |
| if spec.get('side_dim') and spec.get('kind') != 'atom': | |
| raise ValueError('side measurements require the atom projector') | |
| if spec.get('context_layers') and spec.get('kind') != 'atom': | |
| raise ValueError('context residual requires the atom projector') | |
| if spec.get('readout_classes') and spec.get('kind') != 'atom': | |
| raise ValueError('classification readout requires the atom projector') | |
| if spec.get("kind") == "atom-set": | |
| model = AtomSetProjector(spec["feature_dim"], spec["index_dim"], spec["max_atoms"], | |
| spec["model_dim"], spec["layers"], spec["heads"], | |
| spec["ffn_dim"], output_dim, scale) | |
| elif spec.get("kind") == "atom": | |
| model = AtomProjector(spec["feature_dim"], spec["index_dim"], spec["max_atoms"], | |
| spec["hidden_dim"], output_dim, scale, | |
| spec.get("discrete_sizes") or (), spec.get("discrete_dim", 0), | |
| spec.get("depth", 0), spec.get("block_mult", 2), | |
| spec.get("block_exit_norm", False), | |
| spec.get("graph_layers", 0), spec.get("graph_hidden_dim", 128), | |
| spec.get('side_dim', 0), spec.get('side_hidden_dim', 128), | |
| spec.get('context_layers', 0), spec.get('context_dim', 256), | |
| spec.get('context_heads', 8), spec.get('readout_classes', 0), | |
| spec.get('readout_uses_side', True)) | |
| else: | |
| model = LeadProjector(spec["feature_dim"], spec["tokens"], spec["hidden_dim"], output_dim, scale) | |
| if spec.get('task_head_outputs'): | |
| model.task_head = nn.Sequential(nn.LayerNorm(output_dim), nn.Linear(output_dim, spec['task_head_outputs'])) | |
| if spec.get('readout_classes'): | |
| model.requires_grad_(train_core) | |
| model.readout.requires_grad_(False) | |
| model.readout.bridge.requires_grad_(True) | |
| if hasattr(model,'task_head'): | |
| model.task_head.requires_grad_(False) | |
| if train_side_only: | |
| if not spec.get('side_dim'): | |
| raise ValueError('side-only training requires a scientific readout side channel') | |
| model.requires_grad_(False) | |
| model.side.requires_grad_(True) | |
| return model | |
| def load_initial(model, state): | |
| """An original atom checkpoint may initialize only the complete non-graph core.""" | |
| loaded = model.load_state_dict(state, strict=False) | |
| missing = set(loaded.missing_keys) | |
| for prefix in ('graph.', 'task_head.', 'side.', 'context.', 'readout.'): | |
| keys = {key for key in model.state_dict() if key.startswith(prefix)} | |
| if missing & keys and not keys <= missing: | |
| raise ValueError(f'incomplete projector initialization: {loaded}') | |
| missing -= keys | |
| if loaded.unexpected_keys or missing: | |
| raise ValueError(f'incomplete projector initialization: {loaded}') | |