"""MiniCPM5-2B-Jev: Native causal FlashAttention multi-branch decision model with lm_head letter readout."""
from __future__ import annotations
import copy
from datetime import datetime
import json
import os
import re
import string
from pathlib import Path
from typing import Any
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from peft import LoraConfig, PeftModel, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer
DEFAULT_BASE_MODEL = "openbmb/MiniCPM5-2B"
MAX_OPTIONS = 255
MAX_STATE = 1152
MAX_BRANCH = 1664
MAX_PACKED = 2304
SERVE_MAX_STATE = 2560
SERVE_MAX_BRANCH = 3072
SERVE_MAX_PACKED = 3584
_SPECIAL_RE = re.compile(r"<\|([A-Za-z0-9_]+)\|>")
_TYPE_PREFIX_RE = re.compile(r"^\[(choice|noul|score)\]\s*", re.IGNORECASE)
_TOKENIZER_META_CACHE: dict[int, tuple[Any, dict[str, Any]]] = {}
class ContextOverflow(ValueError):
"""Raised when a record exceeds state, branch, or packed token limits."""
def load_tokenizer(name: str = DEFAULT_BASE_MODEL):
try:
return AutoTokenizer.from_pretrained(name, local_files_only=True)
except Exception:
return AutoTokenizer.from_pretrained(name)
def pad_id(tok) -> int:
return tok.pad_token_id if tok.pad_token_id is not None else 0
def user_tokens(tok, text: str) -> list[int]:
"""Tokenize user text while preventing control-token injection."""
safe_text = _SPECIAL_RE.sub(r"<¦\1¦>", str(text or ""))
return tok(safe_text, add_special_tokens=False).input_ids
def tokenizer_decision_meta(tok) -> dict[str, Any]:
"""Build and cache option letter token IDs (A..Z, AA..) and chat template wrappers."""
key = id(tok)
if key in _TOKENIZER_META_CACHE:
return _TOKENIZER_META_CACHE[key][1]
upper = string.ascii_uppercase
candidates = list(upper) + [a + b for a in upper for b in upper]
ans_prefix = tok.encode("Answer: (", add_special_tokens=False)
open_paren = tok.encode("\n(", add_special_tokens=False)
label_names: list[str] = []
label_ids: list[int] = []
seen_ids: set[int] = set()
for cand in candidates:
enc = tok.encode(f"Answer: ({cand}", add_special_tokens=False)
if len(enc) == len(ans_prefix) + 1 and enc[: len(ans_prefix)] == ans_prefix:
tid = enc[-1]
if tid not in seen_ids:
seen_ids.add(tid)
label_names.append(cand)
label_ids.append(tid)
if len(label_names) == MAX_OPTIONS:
break
sentinel = "@@SYSTEMONE_USER_CONTENT@@"
msgs = [{"role": "user", "content": sentinel}]
try:
rendered = tok.apply_chat_template(
msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False
)
except Exception:
rendered = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
head_str, tail_str = rendered.split(sentinel)
for opened, closed in (("\n", "\n\n"), ("", "\n\n")):
if tail_str.endswith(opened):
tail_str += closed
chat_head_ids = tok.encode(head_str + "Context:\n", add_special_tokens=False)
chat_tail_ids = tok.encode(tail_str + "Answer: (", add_special_tokens=False)
meta = {
"label_names": label_names,
"label_ids": label_ids,
"open_paren": open_paren,
"chat_head_ids": chat_head_ids,
"chat_tail_ids": chat_tail_ids,
}
_TOKENIZER_META_CACHE[key] = (tok, meta)
return meta
def _trim_state_tokens(state_tokens: list[int], keep_state: int) -> list[int]:
"""Preserve both the header/rules (first 60%) and most recent events/claim (last 40%) when trimming long states."""
if len(state_tokens) <= keep_state:
return state_tokens
head_n = int(keep_state * 0.6)
tail_n = max(0, keep_state - head_n)
if tail_n == 0:
return state_tokens[:keep_state]
return state_tokens[:head_n] + state_tokens[-tail_n:]
def encode_record(
tok,
rec: dict[str, Any],
max_state: int = MAX_STATE,
max_branch: int = MAX_BRANCH,
strict: bool = False,
) -> dict[str, Any]:
"""Encode a record into shared state tokens S and per-question branches.
Each question branch forms an independent causal sequence S + br_k (mathematically identical to block-causal masking,
while unlocking native hardware-fused is_causal=True FlashAttention on MPS/CUDA with zero 4D mask overhead).
"""
meta = tokenizer_decision_meta(tok)
label_names = meta["label_names"]
label_ids = meta["label_ids"]
open_paren = meta["open_paren"]
chat_head_ids = meta["chat_head_ids"]
chat_tail_ids = meta["chat_tail_ids"]
state_tokens = user_tokens(tok, rec["state"])
if strict and len(chat_head_ids) + len(state_tokens) > max_state:
raise ContextOverflow(
f"state exceeds {max_state} tokens: {len(chat_head_ids) + len(state_tokens)}"
)
keep_state = max(16, max_state - len(chat_head_ids))
S = list(chat_head_ids) + _trim_state_tokens(state_tokens, keep_state)
branches: list[tuple[list[int], int, str]] = []
max_br_len = 0
for q in rec["questions"]:
raw_instr_full = str(q.get("instr", ""))
q_type = str(q.get("type") or "").lower()
if not q_type:
m = _TYPE_PREFIX_RE.match(raw_instr_full)
q_type = m.group(1).lower() if m else "choice"
raw_instr = _TYPE_PREFIX_RE.sub("", raw_instr_full).strip()
if q_type == "score":
q_head = user_tokens(tok, f"\n\nQuestion (ordered rating scale, lowest to highest): {raw_instr}\nOptions:")
elif q_type == "noul":
q_head = user_tokens(tok, f"\n\nQuestion (yes/no judgement): {raw_instr}\nOptions:")
else:
q_head = user_tokens(tok, f"\n\nQuestion: {raw_instr}\nOptions:")
opts = q["options"]
if len(opts) > len(label_names):
raise ContextOverflow(f"Too many options ({len(opts)} > {len(label_names)})")
opt_tokens: list[int] = []
for j, opt_str in enumerate(opts):
clean_opt = str(opt_str).replace("\n", " ").strip()
if j < 26:
opt_tokens.extend(user_tokens(tok, f"\n({label_names[j]}) {clean_opt}"))
else:
opt_tokens.extend(open_paren + [label_ids[j]] + user_tokens(tok, f") {clean_opt}"))
br = q_head + opt_tokens + list(chat_tail_ids)
max_br_len = max(max_br_len, len(br))
branches.append((br, len(opts), q_type))
if len(S) + max_br_len > max_branch:
if strict:
raise ContextOverflow(
f"branch too long: {max_br_len} tokens with {len(S)}-token state (limit {max_branch})"
)
allowed_state = max(64, max_branch - max_br_len - len(chat_head_ids))
S = list(chat_head_ids) + _trim_state_tokens(state_tokens, allowed_state)
ids = list(S)
seg = [0] * len(S)
pos = list(range(len(S)))
decide_idx: list[int] = []
opt_idx: list[list[int]] = []
n_opts: list[int] = []
q_types: list[str] = []
causal_rows: list[list[int]] = []
for k, (br, n_opt, q_type) in enumerate(branches, start=1):
base = len(ids)
p0 = len(S)
br_pos = list(range(p0, p0 + len(br)))
ids.extend(br)
seg.extend([k] * len(br))
pos.extend(br_pos)
decide_idx.append(base + len(br) - 1)
opt_idx.append(list(range(n_opt)))
n_opts.append(n_opt)
q_types.append(q_type)
causal_rows.append(S + br)
return {
"ids": ids,
"seg": seg,
"pos": pos,
"state_ids": S,
"causal_rows": causal_rows,
"decide_idx": decide_idx,
"opt_idx": opt_idx,
"n_opts": n_opts,
"q_types": q_types,
"labels": [int(q.get("label", 0)) for q in rec["questions"]],
"target_probs": [q.get("target_probs") for q in rec["questions"]],
"state_truncated": len(chat_head_ids) + len(state_tokens) > len(S),
}
def record_fits(
rec: dict[str, Any],
tok,
max_state: int = MAX_STATE,
max_branch: int = MAX_BRANCH,
max_packed: int = MAX_PACKED,
) -> bool:
try:
enc = encode_record(tok, rec, max_state=max_state, max_branch=max_branch, strict=True)
return len(enc["ids"]) <= max_packed
except Exception:
return False
class LetterReadoutHead(nn.Module):
"""Native lm_head projection onto option letter tokens (A..Z, AA..) with PriDe prior debiasing and per-type temperature calibration."""
MAX_K_TABLE = 32
def __init__(self, letter_weight: torch.Tensor) -> None:
super().__init__()
n_max = int(letter_weight.shape[0])
self.register_buffer("letter_weight", letter_weight.detach().clone().float())
self.register_buffer("letter_bias", torch.zeros(n_max, dtype=torch.float32))
self.register_buffer(
"choice_k_bias",
torch.zeros(self.MAX_K_TABLE + 1, self.MAX_K_TABLE, dtype=torch.float32),
)
self.register_buffer(
"score_k_bias",
torch.zeros(self.MAX_K_TABLE + 1, self.MAX_K_TABLE, dtype=torch.float32),
)
self.register_buffer("noul_bias", torch.zeros(2, dtype=torch.float32))
self.register_buffer("score_bias", torch.zeros(11, dtype=torch.float32))
self.register_buffer("type_temps", torch.ones(3, dtype=torch.float32)) # [choice, noul, score]
self.temperature = 1.0
self.use_prior = True
def forward_raw(self, h_decide: torch.Tensor, n_opt: int) -> torch.Tensor:
return F.linear(h_decide, self.letter_weight[:n_opt])
def forward(self, h_decide: torch.Tensor, n_opt: int, q_type: str = "choice") -> torch.Tensor:
z = F.linear(h_decide, self.letter_weight[:n_opt])
if self.use_prior and not self.training:
if q_type == "noul" and n_opt == 2:
z = z - self.noul_bias
elif q_type == "score":
if (
2 <= n_opt <= self.MAX_K_TABLE
and torch.any(self.score_k_bias[n_opt, :n_opt] != 0)
):
z = z - self.score_k_bias[n_opt, :n_opt]
elif n_opt == 11 and torch.any(self.score_bias != 0):
z = z - self.score_bias
else:
if (
2 <= n_opt <= self.MAX_K_TABLE
and torch.any(self.choice_k_bias[n_opt, :n_opt] != 0)
):
z = z - self.choice_k_bias[n_opt, :n_opt]
else:
z = z - self.letter_bias[:n_opt]
if self.training:
return z
type_idx = 1 if q_type == "noul" else (2 if q_type == "score" else 0)
t_type = float(self.type_temps[type_idx].item())
t = t_type if t_type != 1.0 else float(self.temperature)
return z if t == 1.0 else z / t
class MiniCPMSystemOne(nn.Module):
"""MiniCPM5-2B backbone + All-Layer LoRA + native causal FlashAttention + LetterReadoutHead."""
SHAPE_BUCKET = int(os.environ.get("MINICPM_S1_SHAPE_BUCKET", "64"))
MAX_ROWS_PER_PASS = int(os.environ.get("MINICPM_S1_MAX_ROWS", "8"))
def __init__(
self,
base_model: str = DEFAULT_BASE_MODEL,
tok=None,
device: str = "mps",
lora_r: int = 0,
head_dim: int = 256,
dtype: torch.dtype = torch.bfloat16,
adapter_dir: str | Path | None = None,
lora_targets: str = "all",
top_k_layers: int = 20,
grad_ckpt: bool = False,
) -> None:
super().__init__()
self.base_model = base_model
self.tok = tok or load_tokenizer(base_model)
self.pad_id = pad_id(self.tok)
self.device = device
self.lora_r = lora_r
self.head_dim = head_dim
self.lora_targets = lora_targets
self.top_k_layers = top_k_layers
meta = tokenizer_decision_meta(self.tok)
label_ids = torch.tensor(meta["label_ids"], dtype=torch.long)
try:
full_lm = AutoModelForCausalLM.from_pretrained(
base_model,
dtype=dtype,
attn_implementation="sdpa",
local_files_only=True,
)
except Exception:
full_lm = AutoModelForCausalLM.from_pretrained(
base_model,
dtype=dtype,
attn_implementation="sdpa",
)
letter_weight = full_lm.lm_head.weight[label_ids].detach().clone().float()
base_lm = full_lm.model
del full_lm
for p in base_lm.parameters():
p.requires_grad_(False)
has_adapter_files = (
lora_r > 0
and adapter_dir is not None
and (Path(adapter_dir) / "adapter_config.json").exists()
and (Path(adapter_dir) / "adapter_model.safetensors").exists()
)
if has_adapter_files:
self.lm = PeftModel.from_pretrained(base_lm, str(adapter_dir), is_trainable=False)
elif lora_r > 0:
if grad_ckpt and hasattr(base_lm, "gradient_checkpointing_enable"):
base_lm.gradient_checkpointing_enable(
gradient_checkpointing_kwargs={"use_reentrant": False}
)
if hasattr(base_lm, "enable_input_require_grads"):
base_lm.enable_input_require_grads()
target_map = {
"attn": ["q_proj", "k_proj", "v_proj", "o_proj"],
"all": ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
}
targets = target_map.get(lora_targets, target_map["all"])
n_layers = len(base_lm.layers)
layers_to_transform = (
list(range(max(0, n_layers - top_k_layers), n_layers))
if 0 < top_k_layers < n_layers
else None
)
try:
cfg = LoraConfig(
task_type="FEATURE_EXTRACTION",
r=lora_r,
lora_alpha=2 * lora_r,
lora_dropout=0.0,
target_modules=targets,
layers_to_transform=layers_to_transform,
layers_pattern="layers" if layers_to_transform else None,
)
self.lm = get_peft_model(base_lm, cfg)
except Exception:
cfg = LoraConfig(
task_type="FEATURE_EXTRACTION",
r=lora_r,
lora_alpha=2 * lora_r,
lora_dropout=0.0,
target_modules=targets,
)
self.lm = get_peft_model(base_lm, cfg)
if 0 < top_k_layers < n_layers:
for layer in base_lm.layers[:-top_k_layers]:
for p in layer.parameters():
p.requires_grad_(False)
else:
self.lm = base_lm
self.head = LetterReadoutHead(letter_weight)
self.to(device)
self.head.float()
def _extract_causal_rows(self, enc: dict[str, Any]) -> list[list[int]]:
if "causal_rows" in enc:
return enc["causal_rows"]
# Fallback if a legacy encoded dict without causal_rows is passed
ids = enc["ids"]
seg = enc["seg"]
state_ids = [tid for tid, s in zip(ids, seg) if s == 0]
rows: list[list[int]] = []
for k in range(1, len(enc["decide_idx"]) + 1):
br_ids = [tid for tid, s in zip(ids, seg) if s == k]
rows.append(state_ids + br_ids)
return rows
def _forward_causal_rows(self, flat_rows: list[list[int]]) -> torch.Tensor:
"""Run a list of causal rows [S + br_k] using native unmasked causal SDPA (is_causal=True)
and return the hidden states at each row's terminal `Answer: (` decision token [N_rows, H].
Right-padding after `len(row) - 1` cannot affect causal positions `< len(row)`.
"""
max_len = max(len(r) for r in flat_rows)
if max_len > 1536:
max_r = 1
elif max_len > 896:
max_r = min(2, self.MAX_ROWS_PER_PASS)
else:
max_r = self.MAX_ROWS_PER_PASS
if len(flat_rows) > max_r:
chunks = [
self._forward_causal_rows(flat_rows[i : i + max_r])
for i in range(0, len(flat_rows), max_r)
]
return torch.cat(chunks, dim=0)
L = max_len
if str(self.device) == "mps" and self.SHAPE_BUCKET > 1:
L = -(-L // self.SHAPE_BUCKET) * self.SHAPE_BUCKET
B = len(flat_rows)
padded = [r + [self.pad_id] * (L - len(r)) for r in flat_rows]
slot_idx = [len(r) - 1 for r in flat_rows]
ids = torch.tensor(padded, dtype=torch.long, device=self.device)
out = self.lm(input_ids=ids, use_cache=False)
h = out.last_hidden_state
batch_idx = torch.arange(B, device=self.device)
slot_ten = torch.tensor(slot_idx, dtype=torch.long, device=self.device)
res = h[batch_idx, slot_ten].float()
if str(self.device) == "mps" and max_len > 1536:
del out, h, ids
torch.mps.empty_cache()
return res
@torch.no_grad()
def _forward_record_eval(self, enc: dict[str, Any]) -> torch.Tensor:
"""Fast inference for a single record: reuses the state prefix KV-cache when a record
has multiple questions and a long state (`len(state_ids) >= 256`), avoiding redundant state encoding.
"""
rows = self._extract_causal_rows(enc)
state_ids = enc.get("state_ids")
if state_ids is None:
ids = enc["ids"]
seg = enc["seg"]
state_ids = [tid for tid, s in zip(ids, seg) if s == 0]
L_s = len(state_ids)
if len(rows) <= 1 or L_s < 256:
return self._forward_causal_rows(rows)
s_ten = torch.tensor([state_ids], dtype=torch.long, device=self.device)
out_s = self.lm(input_ids=s_ten, use_cache=True)
pkv = out_s.past_key_values
h_list: list[torch.Tensor] = []
for r in rows:
br = r[L_s:]
br_ten = torch.tensor([br], dtype=torch.long, device=self.device)
out_b = self.lm(input_ids=br_ten, past_key_values=pkv, use_cache=True)
h_list.append(out_b.last_hidden_state[0, -1].float())
pkv.crop(-len(br))
res = torch.stack(h_list, dim=0)
del out_s, pkv, s_ten
if str(self.device) == "mps":
torch.mps.empty_cache()
return res
def forward_batch(self, encs: list[dict[str, Any]]) -> list[list[torch.Tensor]]:
flat_rows: list[list[int]] = []
counts: list[int] = []
for e in encs:
rows = self._extract_causal_rows(e)
flat_rows.extend(rows)
counts.append(len(rows))
h_decides = self._forward_causal_rows(flat_rows)
out: list[list[torch.Tensor]] = []
offset = 0
for e, c in zip(encs, counts):
n_opts = e.get("n_opts") or [len(oi) for oi in e["opt_idx"]]
q_types = e.get("q_types") or ["choice"] * len(n_opts)
h_sub = h_decides[offset : offset + c]
offset += c
out.append(
[
self.head(h_sub[j], n_opt, q_type=qt)
for j, (n_opt, qt) in enumerate(zip(n_opts, q_types))
]
)
return out
def forward(self, enc: dict[str, Any]) -> list[torch.Tensor]:
return self.forward_batch([enc])[0]
@torch.no_grad()
def raw_logits(self, enc: dict[str, Any]) -> list[torch.Tensor]:
h_decides = self._forward_record_eval(enc)
n_opts = enc.get("n_opts") or [len(oi) for oi in enc["opt_idx"]]
return [self.head.forward_raw(h_decides[j], n_opt).cpu() for j, n_opt in enumerate(n_opts)]
@torch.no_grad()
def raw_logits_batch(self, encs: list[dict[str, Any]]) -> list[list[torch.Tensor]]:
flat_rows: list[list[int]] = []
counts: list[int] = []
for e in encs:
rows = self._extract_causal_rows(e)
flat_rows.extend(rows)
counts.append(len(rows))
h_decides = self._forward_causal_rows(flat_rows)
out: list[list[torch.Tensor]] = []
offset = 0
for e, c in zip(encs, counts):
n_opts = e.get("n_opts") or [len(oi) for oi in e["opt_idx"]]
h_sub = h_decides[offset : offset + c]
offset += c
out.append(
[self.head.forward_raw(h_sub[j], n_opt).cpu() for j, n_opt in enumerate(n_opts)]
)
return out
@torch.no_grad()
def probs(self, enc: dict[str, Any]) -> list[torch.Tensor]:
return [F.softmax(z, dim=-1).cpu() for z in self.forward(enc)]
@torch.no_grad()
def logits_and_probs(self, enc: dict[str, Any]) -> list[tuple[torch.Tensor, torch.Tensor]]:
h_decides = self._forward_record_eval(enc)
n_opts = enc.get("n_opts") or [len(oi) for oi in enc["opt_idx"]]
q_types = enc.get("q_types") or ["choice"] * len(n_opts)
zs = [
self.head(h_decides[j], n_opt, q_type=qt)
for j, (n_opt, qt) in enumerate(zip(n_opts, q_types))
]
return [(z.cpu(), F.softmax(z, dim=-1).cpu()) for z in zs]
@torch.no_grad()
def predict_record(
self,
rec_internal: dict[str, Any],
use_pride: bool = False,
max_state: int = SERVE_MAX_STATE,
max_branch: int = SERVE_MAX_BRANCH,
max_packed: int = SERVE_MAX_PACKED,
) -> list[tuple[torch.Tensor, torch.Tensor]]:
"""Predict (calibrated_logits, probs) for every question in rec_internal using fast native causal rows."""
self.eval()
qs = rec_internal["questions"]
if not qs:
return []
if not use_pride:
enc_full = encode_record(
self.tok, rec_internal, max_state=max_state, max_branch=max_branch, strict=False
)
return self.logits_and_probs(enc_full)
# Exact PriDe cyclic permutation across shifts for choice/noul (preserving ordered scale for score)
all_sub_qs: list[dict[str, Any]] = []
q_specs: list[tuple[str, int, float, list[list[int]] | None]] = []
for q in qs:
opts = list(q["options"])
K = len(opts)
qt = str(q.get("type") or "choice").lower()
type_idx = 1 if qt == "noul" else (2 if qt == "score" else 0)
t_type = float(self.head.type_temps[type_idx].item())
temp = t_type if t_type != 1.0 else float(self.head.temperature)
if qt == "score" or K <= 1 or K > 12:
all_sub_qs.append(q)
q_specs.append((qt, K, temp, None))
else:
perms: list[list[int]] = []
for s in range(K):
perm = [(j + s) % K for j in range(K)]
perms.append(perm)
q_shift = copy.deepcopy(q)
q_shift["options"] = [opts[idx] for idx in perm]
all_sub_qs.append(q_shift)
q_specs.append((qt, K, temp, perms))
sub_enc = encode_record(
self.tok,
{**rec_internal, "questions": all_sub_qs},
max_state=max_state,
max_branch=max_branch,
strict=False,
)
h_decides = self._forward_record_eval(sub_enc)
results_pride: list[tuple[torch.Tensor, torch.Tensor]] = []
offset = 0
for qt, K, temp, perms in q_specs:
if perms is None:
z_cal = self.head(h_decides[offset], K, q_type=qt).cpu()
offset += 1
results_pride.append((z_cal, F.softmax(z_cal, dim=-1)))
else:
unpermed_log_probs = np.zeros((K, K), dtype=np.float64)
for s, perm in enumerate(perms):
z_s = self.head.forward_raw(h_decides[offset + s], K).cpu().numpy().astype(np.float64)
z_c = z_s - np.max(z_s)
log_p = z_c - np.log(np.sum(np.exp(z_c)))
for pos_j, opt_idx in enumerate(perm):
unpermed_log_probs[s, opt_idx] = log_p[pos_j]
offset += K
debiased_logits = unpermed_log_probs.mean(axis=0)
debiased_logits = debiased_logits - debiased_logits.mean()
cal_logits = debiased_logits / max(temp, 1e-4)
z_ten = torch.tensor(cal_logits, dtype=torch.float32)
p_ten = F.softmax(z_ten, dim=-1)
results_pride.append((z_ten, p_ten))
return results_pride
def trainable_parameters(self) -> list[nn.Parameter]:
return [p for p in self.parameters() if p.requires_grad]
def head_parameters(self) -> list[nn.Parameter]:
return [p for p in self.head.parameters() if p.requires_grad]
def lora_parameters(self) -> list[nn.Parameter]:
head_ids = {id(p) for p in self.head.parameters()}
return [p for p in self.parameters() if p.requires_grad and id(p) not in head_ids]
def save_checkpoint(self, out_dir: str | Path, extra_meta: dict[str, Any] | None = None) -> None:
out_path = Path(out_dir)
out_path.mkdir(parents=True, exist_ok=True)
if self.lora_r > 0 and hasattr(self.lm, "save_pretrained"):
self.lm.save_pretrained(str(out_path))
else:
for stale in ("adapter_config.json", "adapter_model.safetensors"):
sp = out_path / stale
if sp.exists():
sp.unlink()
type_temps_list = [round(float(x), 4) for x in self.head.type_temps.detach().cpu().tolist()]
payload = {
"state_dict": self.head.state_dict(),
"base_model": self.base_model,
"lora_r": self.lora_r,
"lora_targets": self.lora_targets,
"top_k_layers": self.top_k_layers,
"head_dim": self.head_dim,
"readout": "native_lm_head_letters_pride",
"temperature": float(self.head.temperature),
"type_temperatures": {
"choice": type_temps_list[0],
"noul": type_temps_list[1],
"score": type_temps_list[2],
},
"meta": extra_meta or {},
}
torch.save(payload, out_path / "head.pt")
with (out_path / "checkpoint_meta.json").open("w", encoding="utf-8") as f:
json.dump(
{
"base_model": self.base_model,
"lora_r": self.lora_r,
"lora_targets": self.lora_targets,
"top_k_layers": self.top_k_layers,
"head_dim": self.head_dim,
"readout": "native_lm_head_letters_pride",
"temperature": float(self.head.temperature),
"type_temperatures": {
"choice": type_temps_list[0],
"noul": type_temps_list[1],
"score": type_temps_list[2],
},
**(extra_meta or {}),
},
f,
indent=2,
ensure_ascii=False,
)
@classmethod
def load_checkpoint(
cls,
ckpt_dir: str | Path,
device: str = "mps",
dtype: torch.dtype = torch.bfloat16,
) -> tuple["MiniCPMSystemOne", Any]:
ckpt_path = Path(ckpt_dir)
head_pt = ckpt_path / "head.pt"
if head_pt.exists():
head_data = torch.load(head_pt, map_location="cpu", weights_only=True)
else:
head_data = {}
base_model = head_data.get("base_model", DEFAULT_BASE_MODEL)
lora_r = int(head_data.get("lora_r", 0))
lora_targets = str(head_data.get("lora_targets", "all"))
top_k_layers = int(head_data.get("top_k_layers", 0))
head_dim = int(head_data.get("head_dim", 256))
tok = load_tokenizer(base_model)
has_adapter = (
lora_r > 0
and (ckpt_path / "adapter_config.json").exists()
and (ckpt_path / "adapter_model.safetensors").exists()
)
model = cls(
base_model=base_model,
tok=tok,
device=device,
lora_r=lora_r,
head_dim=head_dim,
dtype=dtype,
adapter_dir=ckpt_path if has_adapter else None,
lora_targets=lora_targets,
top_k_layers=top_k_layers,
grad_ckpt=False,
)
if "state_dict" in head_data and "letter_weight" in head_data["state_dict"]:
model.head.load_state_dict(head_data["state_dict"], strict=False)
model.head.to(device)
model.head.temperature = float(
os.environ.get("MINICPM_S1_TEMPERATURE") or head_data.get("temperature", 1.0)
)
if os.environ.get("MINICPM_S1_TEMPERATURE"):
t_override = float(os.environ["MINICPM_S1_TEMPERATURE"])
model.head.type_temps.fill_(t_override)
model.eval()
return model, tok
SCORE_OPTIONS = [f"{i}: score {i} out of 10" for i in range(11)]
_LEVEL_PREFIX_RE = re.compile(r"^\s*-?\d+\s*:")
_NORM_TOKEN_RE = re.compile(r"[\s_\.\-/]+")
_MONTHS = "January|February|March|April|May|June|July|August|September|October|November|December"
_DATE_RE = re.compile(
rf"\b(?:{_MONTHS}) \d{{1,2}}, \d{{4}}\b|\b\d{{1,2}} (?:{_MONTHS}) \d{{4}}\b|\b\d{{4}}-\d{{2}}-\d{{2}}\b"
)
def _norm_key(s: str) -> str:
return _NORM_TOKEN_RE.sub(" ", str(s or "").strip().lower()).strip()
def compute_date_facts(text: str, max_dates: int = 7) -> str:
"""Deterministic date difference helper for states containing 2..max_dates absolute dates."""
found: list[tuple[str, datetime]] = []
for m in _DATE_RE.finditer(text):
raw = m.group(0)
d = None
for fmt in ("%B %d, %Y", "%d %B %Y", "%Y-%m-%d"):
try:
d = datetime.strptime(raw, fmt)
break
except ValueError:
continue
if d is not None and raw not in [r for r, _ in found]:
found.append((raw, d))
if len(found) >= max_dates:
break
if len(found) < 2:
return ""
facts: list[str] = []
for i in range(len(found)):
for j in range(i + 1, len(found)):
n = (found[j][1] - found[i][1]).days
if n == 0:
facts.append(f"{found[j][0]} is the same day as {found[i][0]}.")
else:
direction = "after" if n > 0 else "before"
facts.append(
f"{found[j][0]} is {abs(n)} day{'s' if abs(n) != 1 else ''} {direction} {found[i][0]}."
)
return " ".join(facts[:10])
def render_json_content(v: Any, indent: int = 0) -> str:
"""Flatten str | dict | list into clean text, unpacking JSON-serialized string objects when present."""
pad = " " * indent
if v is None:
return ""
if isinstance(v, str) and indent == 0:
s_strip = v.strip()
if (s_strip.startswith("{") and s_strip.endswith("}")) or (
s_strip.startswith("[") and s_strip.endswith("]")
):
try:
parsed = json.loads(s_strip)
if isinstance(parsed, (dict, list)):
return render_json_content(parsed, indent=0)
except Exception:
pass
return v
if isinstance(v, (str, int, float, bool)):
return str(v)
if isinstance(v, list):
return "\n".join(f"{pad}- {render_json_content(x, indent + 1).lstrip()}" for x in v)
if isinstance(v, dict):
return "\n".join(
f"{pad}{k}:\n{render_json_content(x, indent + 1)}"
if isinstance(x, (dict, list))
else f"{pad}{k}: {render_json_content(x)}"
for k, x in v.items()
)
return str(v)
def option_text(name: str, desc: Any) -> str:
"""Render option name and optional description while suppressing redundant 'name: name' duplicates."""
if desc is None or desc == "":
return str(name)
rendered = render_json_content(desc).strip()
if not rendered or _norm_key(rendered) == _norm_key(name):
return str(name)
return f"{name}: {rendered}"
def to_internal_record(rec: dict[str, Any], add_date_facts: bool = False) -> dict[str, Any]:
"""Convert wire-format record (questions dict) into internal question list for encoding."""
qs = []
for name, q in rec["questions"].items():
qtype = str(q["type"]).lower()
instr_str = render_json_content(q.get("instructions"))
if not instr_str and qtype == "noul":
instr_str = "Which answer fits the context?"
target_probs: list[float] | None = None
if qtype == "choice":
crit = q.get("criteria") or {}
if isinstance(crit, list):
crit = {str(x): None for x in crit}
keys = list(crit.keys())
opts = [option_text(str(k), crit[k]) for k in keys]
lbl = q.get("label", q.get("answer", keys[0] if keys else 0))
label = keys.index(lbl) if lbl in keys else int(lbl)
elif qtype == "noul":
crit = q.get("criteria") if isinstance(q.get("criteria"), dict) else {}
keys = ["false", "true"]
f_desc = crit.get("false", crit.get(False, "false, negative, does not hold"))
t_desc = crit.get("true", crit.get(True, "true, affirmative, holds"))
opts = [
option_text("no", f_desc or "false, negative, does not hold"),
option_text("yes", t_desc or "true, affirmative, holds"),
]
lbl = q.get("label", q.get("answer", False))
if isinstance(lbl, bool):
label = int(lbl)
elif str(lbl).lower() in ("true", "yes", "1"):
label = 1
else:
label = 0
elif qtype == "score":
crit = q.get("criteria")
if isinstance(crit, dict):
crit = [crit[k] for k in sorted(crit, key=lambda x: float(x))]
if isinstance(crit, list) and crit:
opts = []
for i, x in enumerate(crit):
rendered_x = render_json_content(x).strip()
if _LEVEL_PREFIX_RE.match(rendered_x):
opts.append(rendered_x)
else:
opts.append(f"{i}: {rendered_x}")
keys = [str(i) for i in range(len(opts))]
else:
opts = list(SCORE_OPTIONS)
keys = [str(i) for i in range(11)]
label = int(q.get("label", q.get("answer", 0)))
else:
raise ValueError(f"Unknown question type: {qtype}")
if isinstance(q.get("target_probs"), list) and len(q["target_probs"]) == len(opts):
target_probs = [float(p) for p in q["target_probs"]]
elif isinstance(q.get("target"), dict):
tgt_map = q["target"]
probs_raw = [float(tgt_map.get(k, 1.0 / len(keys))) for k in keys]
s_tot = sum(probs_raw) or 1.0
target_probs = [p / s_tot for p in probs_raw]
qs.append(
{
"name": name,
"type": qtype,
"instr": f"[{qtype}] {instr_str}",
"options": opts,
"keys": keys,
"label": max(0, min(label, len(opts) - 1)),
"target_probs": target_probs,
}
)
meta = rec.get("_meta") or {}
state_str = render_json_content(rec["state"])
if add_date_facts and "date_facts:" not in state_str:
df = compute_date_facts(state_str)
if df:
state_str = f"{state_str}\n\ndate_facts: {df}"
return {
"id": rec.get("id") or meta.get("id", ""),
"source": rec.get("source") or meta.get("source", "custom"),
"state": state_str,
"questions": qs,
}