"""Graft: splice connector soft tokens into the frozen Jais-3-0.9B (K2HorizonForCausalLM) through inputs_embeds. Prompt (same chat format as serve_mix2.prompt / train_lora.encode, fastest thinking mode): <|begin_of_text|><|ifm|im_start|>system\n{SYSTEM}<|ifm|im_end|><|ifm|im_start|>user\n [soft x144] {question} <|ifm|im_end|><|ifm|im_start|>assistant\n\n {answer}<|ifm|im_end|> The two marker rows (64031/64032) are replaced at splice time by two learned vectors (initialised from the real rows); the embedding matrix itself is never unfrozen. Labels only on the answer (+ end-of-turn). Right padding for training, left padding (with correct position ids) for generation.""" import os, sys, torch, torch.nn as nn, torch.nn.functional as F HERE = os.path.dirname(os.path.abspath(__file__)); BRAIN = os.path.dirname(HERE) if HERE not in sys.path: sys.path.insert(0, HERE) from connector import Connector # Local Jais copy if JAIS_PATH is set and exists; otherwise the public Hub repo (auto-downloaded). BASE = os.environ.get("JAIS_PATH") or os.path.join(BRAIN, "jais3_09b") if not os.path.isdir(BASE): BASE = os.environ.get("JAIS_REPO") or "inception42/Jais-3-0.9B" IMG, IMG_END = 64031, 64032 S, EN = "<|ifm|im_start|>", "<|ifm|im_end|>" SYSTEM = "أنت «تصحيح»، مساعد عربي للكتابة واللغة. تتكلم بطبيعية واختصار مثل صديق، بلهجة المستخدم نفسها، وبالعربية فقط." # same default as train_lora.py HEADER = "\n" def load_trunk(device="cpu", dtype=None, layers=None): """TRUNK_LAYERS / TRUNK_DTYPE=fp32 are TEST-ONLY knobs (a truncated real-weight trunk in fp32 makes CPU tests fast; the CPU has no bf16 units)""" from transformers import AutoTokenizer, AutoModelForCausalLM dtype = dtype or (torch.float32 if os.environ.get("TRUNK_DTYPE") == "fp32" else torch.bfloat16); layers = layers or int(os.environ.get("TRUNK_LAYERS", 0)) or None tok = AutoTokenizer.from_pretrained(BASE, trust_remote_code=True) m = AutoModelForCausalLM.from_pretrained(BASE, trust_remote_code=True, dtype=dtype).to(device).eval() if layers: m.model.layers = m.model.layers[:layers]; m.config.num_hidden_layers = layers for p in m.parameters(): p.requires_grad_(False) return tok, m class Graft(nn.Module): def __init__(self, trunk, tok, connector=None, system=SYSTEM): super().__init__() object.__setattr__(self, "trunk", trunk) # held by reference, NOT a submodule: Graft.parameters() = connector + markers only self.tok = tok; self.system = system dev = trunk.get_input_embeddings().weight.device self.connector = (connector or Connector()).to(dev) E = trunk.get_input_embeddings().weight self.markers = nn.Parameter(E[[IMG, IMG_END]].detach().float().clone()) # [2,1536] learned and vectors self.end_id = tok.convert_tokens_to_ids(EN) self._ids = lambda s: tok(s, add_special_tokens=False).input_ids # ------------------------------------------------------------------ prompt pieces def pieces(self, question, answer=None, has_image=True): """-> (pre ids, post ids, answer ids). pre ends right before ; post starts right after .""" ids = self._ids head = ids("<|begin_of_text|>" + S + "system\n" + self.system + EN + S + "user\n") tail = ids(("" if not has_image else "") + question + EN + S + "assistant\n" + HEADER) ans = ids(answer + EN) if answer is not None else [] return head, tail, ans def text_ids(self, question): """the ids of the SAME prompt with no image (used by the G0 parity test)""" head, tail, _ = self.pieces(question, has_image=False) return head + tail # ------------------------------------------------------------------ embeddings def _embed(self, ids): E = self.trunk.get_input_embeddings() with torch.no_grad(): return E(torch.tensor(ids, device=E.weight.device)) def build(self, feats, questions, answers=None, low=False, left_pad=False): """feats [B,576,768] (or None -> text only); returns dict(inputs_embeds, attention_mask, position_ids, labels, n_img) dtype of inputs_embeds = trunk dtype. Gradients flow to connector + markers only.""" dt = self.trunk.get_input_embeddings().weight.dtype; dev = self.markers.device soft = self.connector(feats.to(dev), low=low) if feats is not None else None n_img = soft.shape[1] if soft is not None else 0 rows, labs = [], [] for b, q in enumerate(questions): head, tail, ans = self.pieces(q, None if answers is None else answers[b], has_image=feats is not None) parts = [self._embed(head)] if soft is not None: parts += [self.markers[0:1].to(dt), soft[b].to(dt), self.markers[1:2].to(dt)] parts += [self._embed(tail)] if ans: parts += [self._embed(ans)] e = torch.cat(parts, 0); rows.append(e) L = e.shape[0]; y = torch.full((L,), -100, dtype=torch.long); if ans: y[L - len(ans):] = torch.tensor(ans) labs.append(y) T = max(r.shape[0] for r in rows); B = len(rows); H = rows[0].shape[1] emb = torch.zeros(B, T, H, dtype=dt, device=dev); att = torch.zeros(B, T, dtype=torch.long); lab = torch.full((B, T), -100, dtype=torch.long) for b, (r, y) in enumerate(zip(rows, labs)): L = r.shape[0]; s = T - L if left_pad else 0 emb[b, s:s + L] = r; att[b, s:s + L] = 1; lab[b, s:s + L] = y att = att.to(dev); lab = lab.to(dev) pos = (att.cumsum(1) - 1).clamp_min(0) return dict(inputs_embeds=emb, attention_mask=att, position_ids=pos, labels=lab, n_img=n_img) # ------------------------------------------------------------------ loss (logits only at answer positions) def loss(self, feats, questions, answers, low=False, return_tokens=False): d = self.build(feats, questions, answers, low=low) h = self.trunk.model(inputs_embeds=d["inputs_embeds"], attention_mask=d["attention_mask"], position_ids=d["position_ids"], use_cache=False).last_hidden_state lab = d["labels"][:, 1:]; hs = h[:, :-1] m = lab != -100 logits = self.trunk.lm_head(hs[m]).float() ce = F.cross_entropy(logits, lab[m], reduction="sum") n = int(m.sum()) return (ce / max(n, 1), ce, n) if return_tokens else ce / max(n, 1) # ------------------------------------------------------------------ greedy generation (own loop: inputs_embeds + KV cache + left padding) def iter_tokens(self, feats, questions, max_new=48, low=False, use_cache=True, stop_ids=None, step_ctx=None): """generator: yields a list with the next token id of every batch row at each step (rows that already stopped repeat their stop token). step_ctx() -> context manager entered around each forward only (so no_grad / autocast / adapter-off never leak to the caller while suspended).""" import contextlib from transformers import DynamicCache step_ctx = step_ctx or contextlib.nullcontext with torch.no_grad(): d = self.build(feats, questions, None, low=low, left_pad=True) emb, att, pos = d["inputs_embeds"], d["attention_mask"], d["position_ids"] B = emb.shape[0]; stop = {self.end_id, self.tok.eos_token_id, *(stop_ids or [])} E = self.trunk.get_input_embeddings(); done = [False] * B cache = DynamicCache(config=self.trunk.config) if use_cache else None cur_emb, cur_att, cur_pos = emb, att, pos; last = pos[:, -1:] for step in range(max_new): with torch.no_grad(), step_ctx(): o = self.trunk.model(inputs_embeds=cur_emb, attention_mask=cur_att, position_ids=cur_pos, past_key_values=cache, use_cache=use_cache) nxt = self.trunk.lm_head(o.last_hidden_state[:, -1]).float().argmax(-1) ne = E(nxt).unsqueeze(1) toks = [int(t) for t in nxt]; yield toks for b, t in enumerate(toks): done[b] = done[b] or t in stop if all(done): return last = last + 1 cur_att = torch.cat([cur_att, torch.ones(B, 1, dtype=cur_att.dtype, device=cur_att.device)], 1) if use_cache: cur_emb, cur_pos = ne, last else: emb = torch.cat([emb, ne], 1); pos = torch.cat([pos, last], 1); cur_emb, cur_pos = emb, pos def generate(self, feats, questions, max_new=48, low=False, use_cache=True, return_ids=False, stop_ids=None, step_ctx=None): stop = {self.end_id, self.tok.eos_token_id, *(stop_ids or [])}; out = [[] for _ in questions]; fin = [False] * len(questions) for toks in self.iter_tokens(feats, questions, max_new, low, use_cache, stop_ids, step_ctx): for b, t in enumerate(toks): if not fin[b]: out[b].append(t); fin[b] = t in stop if return_ids: return out return [self.tok.decode([i for i in ids if i not in stop], skip_special_tokens=True).strip() for ids in out] # ------------------------------------------------------------------ trainable state def trainable(self): return [p for p in self.parameters() if p.requires_grad] def state(self): return {"connector": self.connector.state_dict(), "markers": self.markers.detach().cpu()} def load_state(self, st): self.connector.load_state_dict(st["connector"]); self.markers.data.copy_(st["markers"].to(self.markers.device))