| """ |
| Cerebellum-2B (小脑-2B): Sub-30ms Non-Autoregressive Agent Decision Model |
| Developed on Qwen3.5-2B Backbone with Symmetric Cross-Option SetPointerHead and Chow's Rejection Gate. |
| """ |
|
|
| import os, time, math, re |
| from dataclasses import dataclass |
| from typing import List, Dict, Optional, Union |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from transformers import AutoModelForCausalLM, AutoTokenizer, PreTrainedModel, PretrainedConfig |
|
|
| SPECIAL = ["<|fim_prefix|>", "<|fim_middle|>", "<|box_start|>", "<|box_end|>", "<|fim_suffix|>"] |
|
|
| @dataclass |
| class CerebellumDecision: |
| action: str |
| action_index: int |
| confidence: float |
| probabilities: Dict[str, float] |
| needs_escalation: bool |
| escalate_probability: float |
| latency_ms: float |
|
|
| class SetPointerHead(nn.Module): |
| """ |
| Permutation-Equivariant Set Transformer Readout Head |
| - Uses Cross-Option Self-Attention WITHOUT positional encoding |
| - Mathematically guarantees 0.000 Option-Order Flip Rate! |
| """ |
| def __init__(self, d=2048, dp=512, n_heads=4): |
| super().__init__() |
| self.d = d |
| self.dp = dp |
| self.q_proj = nn.Sequential( |
| nn.Linear(d, dp), |
| nn.GELU(), |
| nn.RMSNorm(dp), |
| nn.Linear(dp, dp) |
| ) |
| self.opt_in_proj = nn.Linear(d, dp) |
| self.set_attn = nn.MultiheadAttention(embed_dim=dp, num_heads=n_heads, batch_first=True) |
| self.norm1 = nn.RMSNorm(dp) |
| self.ffn = nn.Sequential( |
| nn.Linear(dp, dp * 2), |
| nn.GELU(), |
| nn.Linear(dp * 2, dp) |
| ) |
| self.norm2 = nn.RMSNorm(dp) |
| self.scale = 1.0 / (dp ** 0.5) |
|
|
| def forward(self, h_decide, h_opts): |
| q = self.q_proj(h_decide) |
| x_opts = self.opt_in_proj(h_opts).unsqueeze(0) |
| attn_out, _ = self.set_attn(x_opts, x_opts, x_opts) |
| x_opts = self.norm1(x_opts + attn_out) |
| x_opts = self.norm2(x_opts + self.ffn(x_opts)).squeeze(0) |
| logits = (x_opts @ q) * self.scale |
| return logits |
|
|
| class ActEscalateHead(nn.Module): |
| """ |
| Chow's Optimal Rejection Gate: |
| Computes distribution sufficient statistics + State representation: |
| 1. Top-1 Probability: max(p) |
| 2. Top-2 Margin: p_top1 - p_top2 |
| 3. Normalized Shannon Entropy: H(p) / log(K) |
| 4. Candidate Option Budget: K / 32 |
| Outputs: [P(Act), P(Escalate)] |
| """ |
| def __init__(self, d=2048, dp=128): |
| super().__init__() |
| self.state_proj = nn.Linear(d, dp) |
| self.mlp = nn.Sequential( |
| nn.Linear(dp + 4, 128), |
| nn.GELU(), |
| nn.Linear(128, 2) |
| ) |
|
|
| def forward(self, h_decide, logits): |
| p = F.softmax(logits, dim=-1) |
| K = p.shape[0] |
| top1 = p.max() |
| if K > 1: |
| top2 = torch.topk(p, 2).values[1] |
| margin = top1 - top2 |
| else: |
| margin = torch.tensor(1.0, device=p.device) |
| |
| entropy = -(p * torch.log(p.clamp(min=1e-9))).sum() |
| norm_entropy = entropy / math.log(max(K, 2)) |
| budget = torch.tensor(min(K / 32.0, 1.0), device=p.device, dtype=h_decide.dtype) |
| |
| stats = torch.stack([top1, margin, norm_entropy, budget]).to(h_decide.dtype) |
| h_proj = self.state_proj(h_decide) |
| feat = torch.cat([h_proj, stats], dim=-1) |
| esc_logits = self.mlp(feat) |
| return esc_logits |
|
|
| def bidirectional_state_branch_mask_batch(segs, device, dtype=torch.bfloat16): |
| L = max(len(s) for s in segs) |
| s = torch.full((len(segs), L), -1, device=device, dtype=torch.long) |
| for b, seg in enumerate(segs): |
| s[b, :len(seg)] = torch.tensor(seg, device=device, dtype=torch.long) |
| |
| q_seg = s[:, :, None] |
| k_seg = s[:, None, :] |
| valid_key = (k_seg != -1) |
| valid_q = (q_seg != -1) |
| |
| state_to_state = (q_seg == 0) & (k_seg == 0) |
| q_to_state = (q_seg > 0) & (k_seg == 0) |
| within_branch = (q_seg > 0) & (q_seg == k_seg) |
| |
| allow = (state_to_state | q_to_state | within_branch) & valid_key & valid_q |
| allow = allow | torch.eye(L, dtype=torch.bool, device=device)[None] |
| |
| mask = torch.zeros((len(segs), 1, L, L), dtype=dtype, device=device) |
| mask_min = -1e4 if dtype == torch.float16 else -1e9 |
| mask.masked_fill_(~allow[:, None, :, :], mask_min) |
| return mask |
|
|
| class CerebellumModel(nn.Module): |
| """ |
| Cerebellum-2B End-to-End Decision Model |
| Provides single-forward-pass sub-30ms decision making for AI Agents. |
| """ |
| def __init__( |
| self, |
| model_dir: str, |
| device: Optional[Union[str, torch.device]] = None, |
| dtype: Optional[torch.dtype] = None, |
| **kwargs |
| ): |
| super().__init__() |
| |
| if device is None: |
| if torch.cuda.is_available(): |
| device = "cuda:0" |
| elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): |
| device = "mps" |
| else: |
| device = "cpu" |
| |
| self.device = torch.device(device) |
| |
| |
| if dtype is None: |
| if self.device.type == "cuda" and torch.cuda.is_bf16_supported(): |
| dtype = torch.bfloat16 |
| elif self.device.type == "mps": |
| dtype = torch.float16 |
| else: |
| dtype = torch.float32 |
| self.dtype = dtype |
|
|
| is_local = os.path.exists(model_dir) |
| self.tok = AutoTokenizer.from_pretrained(model_dir, local_files_only=is_local, trust_remote_code=True) |
| |
| |
| if self.device.type == "cuda": |
| causal_lm = AutoModelForCausalLM.from_pretrained( |
| model_dir, |
| torch_dtype=dtype, |
| device_map=self.device, |
| local_files_only=is_local, |
| trust_remote_code=True |
| ) |
| else: |
| causal_lm = AutoModelForCausalLM.from_pretrained( |
| model_dir, |
| torch_dtype=dtype, |
| local_files_only=is_local, |
| trust_remote_code=True |
| ).to(self.device) |
| |
| self.base_model = causal_lm.model |
| |
| |
| heads_path = os.path.join(model_dir, "heads.pt") |
| if not os.path.exists(heads_path): |
| try: |
| from huggingface_hub import hf_hub_download |
| heads_path = hf_hub_download(repo_id=model_dir, filename="heads.pt") |
| except Exception: |
| pass |
| |
| heads_data = torch.load(heads_path, map_location=self.device) |
| |
| self.pointer_head = SetPointerHead(2048, 512, n_heads=4).to(self.device).to(dtype) |
| self.escalate_head = ActEscalateHead(2048, 128).to(self.device).to(dtype) |
| |
| self.pointer_head.load_state_dict(heads_data["pointer_head"]) |
| self.escalate_head.load_state_dict(heads_data["escalate_head"]) |
| |
| self.pointer_head.eval() |
| self.escalate_head.eval() |
| self.base_model.eval() |
| self.pad_id = self.tok.pad_token_id if self.tok.pad_token_id is not None else 0 |
|
|
| @classmethod |
| def from_pretrained( |
| cls, |
| pretrained_model_name_or_path: str, |
| *args, |
| device: Optional[Union[str, torch.device]] = None, |
| dtype: Optional[torch.dtype] = None, |
| **kwargs |
| ): |
| return cls(model_dir=pretrained_model_name_or_path, device=device, dtype=dtype, **kwargs) |
|
|
| def _encode_query(self, state: str, candidates: List[str], instruction: str = "Select the best action to execute next."): |
| special_re = re.compile(r"<|([A-Za-z0-9_]+)|>") |
| def utok(text): |
| return self.tok(special_re.sub(r"<¦\1¦>", text), add_special_tokens=False).input_ids |
|
|
| raw_state = utok(state) |
| max_state = 1024 |
| if len(raw_state) > max_state - 1: |
| half = (max_state - 1) // 2 |
| state_tokens = raw_state[:half] + raw_state[-half:] |
| else: |
| state_tokens = raw_state |
|
|
| S = [self.tok.convert_tokens_to_ids(SPECIAL[0])] + state_tokens |
| ids, seg, pos = list(S), [0] * len(S), list(range(len(S))) |
| q_id, o_id, c_id, d_id = (self.tok.convert_tokens_to_ids(t) for t in SPECIAL[1:]) |
| |
| br = [q_id] + utok(instruction) |
| oi = [] |
| for o in candidates: |
| o_tok = utok(o) |
| if len(o_tok) > 64: |
| o_tok = o_tok[:32] + o_tok[-32:] |
| br += [o_id] + o_tok + [c_id] |
| oi.append(len(br) - 1) |
| br.append(d_id) |
| |
| base = len(ids) |
| ids += br |
| seg += [1] * len(br) |
| pos += list(range(len(S), len(S) + len(br))) |
| decide_idx = base + len(br) - 1 |
| opt_idx = [base + i for i in oi] |
| |
| return { |
| "ids": ids, |
| "seg": seg, |
| "pos": pos, |
| "decide_idx": decide_idx, |
| "opt_idx": opt_idx |
| } |
|
|
| @torch.inference_mode() |
| def decide( |
| self, |
| state: str, |
| candidates: List[str], |
| instruction: str = "Select the best action to execute next.", |
| escalate_threshold: float = 0.50 |
| ) -> CerebellumDecision: |
| t0 = time.perf_counter() |
| enc = self._encode_query(state, candidates, instruction) |
| |
| ids = torch.tensor([enc["ids"]], device=self.device, dtype=torch.long) |
| pos = torch.tensor([enc["pos"]], device=self.device, dtype=torch.long) |
| mask = bidirectional_state_branch_mask_batch([enc["seg"]], self.device, dtype=self.dtype) |
| |
| hidden = self.base_model(input_ids=ids, position_ids=pos, attention_mask=mask).last_hidden_state[0] |
| h_d = hidden[enc["decide_idx"]] |
| h_o = hidden[torch.tensor(enc["opt_idx"], device=self.device)] |
| |
| logits = self.pointer_head(h_d, h_o) |
| esc_logits = self.escalate_head(h_d, logits) |
| |
| probs = F.softmax(logits, dim=-1).cpu().tolist() |
| esc_probs = F.softmax(esc_logits, dim=-1).cpu().tolist() |
| |
| best_idx = int(torch.argmax(logits).item()) |
| confidence = probs[best_idx] |
| p_escalate = esc_probs[1] |
| needs_escalation = (p_escalate >= escalate_threshold) or (confidence < 0.65) |
| |
| latency = (time.perf_counter() - t0) * 1000.0 |
| |
| prob_dict = {cand: p for cand, p in zip(candidates, probs)} |
| |
| return CerebellumDecision( |
| action=candidates[best_idx], |
| action_index=best_idx, |
| confidence=confidence, |
| probabilities=prob_dict, |
| needs_escalation=needs_escalation, |
| escalate_probability=p_escalate, |
| latency_ms=latency |
| ) |
|
|