Granite-1B-LoopAttnRes / modeling_granite_loop.py
tysgydke's picture
Add HF card; mirror of ModelScope tysgydk/Granite-1B-LoopAttnRes
227ce7f verified
Raw History Blame Contribute Delete
22.6 kB
"""Granite-1B-A400M 的 Looped × AttnRes 改造(消融实验 G0-G3 共用一份代码)。
开关(通过 from_pretrained 的 kwargs 直接传给 __init__):
- loop_rounds: int = 1 循环轮数; 1 = G0 基线
- attnres: bool = False 轮内 Block AttnRes (G2+)
- cross_round: bool = False 源集合加入之前所有轮次的块摘要 (G3)
- attnres_blocks: int = 8 AttnRes 块数 (48 子层 / 8 块 = 6 子层每块)
结构要点(依据 DESIGN.md 与两篇论文):
- 循环: 24 层 × R 轮, 权重跨轮硬共享; 每轮输出过共享 lm_head 算 CE, 取各轮均值
- AttnRes: 每子层(attn/MoE 各算一个)一个零初始化可学习伪查询, 按轮独立;
key=value=子层输出, 核 exp(q·RMSNorm(k)) (RMSNorm 无可学习参数), softmax 在源维度
- residual_multiplier μ=0.22 折进 value (v_i = μ·f_i)
- 块内: 部分和按标准残差累加; 块边界: 块摘要加入源集合
- 每轮末尾: 用独立的终读出伪查询对 [b0] + (跨轮时含历史块) + 本轮块摘要 做最后读出
- 跨轮(G3): 源 = [b0(embedding)] + [之前所有轮次的块摘要] + [本轮已完成块] + [本块部分和]
注意: 训练前向 (input_ids/labels) 不支持 KV cache; 推理用 greedy_generate(手动贪心 + KV cache)。
"""
import torch
import torch.nn.functional as F
from torch import nn
from torch.utils.checkpoint import checkpoint
from types import SimpleNamespace
from transformers.models.granitemoe.configuration_granitemoe import GraniteMoeConfig
from transformers.models.granitemoe.modeling_granitemoe import (
GraniteMoeForCausalLM,
load_balancing_loss_func,
)
from transformers.masking_utils import create_causal_mask
def _rms_norm(x, eps=1e-6):
"""无可学习参数的 RMSNorm(AttnRes 核里用在 key 上)。"""
return x * torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + eps).to(x.dtype)
class GraniteLoopForCausalLM(GraniteMoeForCausalLM):
def __init__(self, config: GraniteMoeConfig, loop_rounds: int = 1, attnres: bool = False,
cross_round: bool = False, attnres_blocks: int = 8, **kwargs):
super().__init__(config)
# 开关: kwargs 优先, 否则读 config(兼容加载我们自己 save_pretrained 的 checkpoint)
self.loop_rounds = getattr(config, "loop_rounds", None) or loop_rounds
self.use_attnres = getattr(config, "attnres", None)
if self.use_attnres is None:
self.use_attnres = attnres
self.cross_round = getattr(config, "cross_round", None)
if self.cross_round is None:
self.cross_round = cross_round
self.attnres_blocks = getattr(config, "attnres_blocks", None) or attnres_blocks
# 写回 config, 使 save_pretrained 能持久化开关
config.loop_rounds = self.loop_rounds
config.attnres = self.use_attnres
config.cross_round = self.cross_round
config.attnres_blocks = self.attnres_blocks
L = config.num_hidden_layers
assert (2 * L) % self.attnres_blocks == 0, "子层数需被块数整除"
self.sub_per_block = (2 * L) // self.attnres_blocks # 每块子层数
assert self.sub_per_block % 2 == 0
self.layers_per_block = self.sub_per_block // 2
d = config.hidden_size
self.alpha_recorder = None # 分析钩子: list 时 _attnres_step 会记录各源注意力权重
self.round_recorder = None # 分析钩子: list 时 forward 会记录每轮结束后的 hidden
if self.use_attnres:
# 伪查询: (轮数, 子层数 + 1, d), 零初始化 -> 初始为等权平均
# +1 = 每轮结束时的终读出查询(对 [b0] + 各块摘要 做最后一次注意力,
# 否则最终 hidden 只剩最后一个块的部分和, 丢失 b0 主干 -> 初始 loss 崩到随机水平)
self.pseudo_queries = nn.Parameter(
torch.zeros(self.loop_rounds, 2 * L + 1, d)
)
@torch.no_grad()
def reset_pseudo_queries(self):
"""from_pretrained 后必须调用: transformers 5 的加载管线会把 checkpoint 里
缺失的 pseudo_queries 重置为未初始化内存(可能含 NaN), 这里重新归零,
恢复论文要求的零初始化(初始 = 等权平均)。自己 save_pretrained 的
checkpoint 含训练后的值, 加载后无需调用。"""
if self.use_attnres:
self.pseudo_queries.zero_()
# ---------- 单个子层前向 ----------
def _sublayer(self, layer, kind, h, mask, pos_emb):
if kind == "attn":
return layer.self_attn(
hidden_states=layer.input_layernorm(h),
attention_mask=mask,
position_embeddings=pos_emb,
)[0]
# moe: 复制 GraniteMoeMoE.forward 但保留 router_logits(aux loss 用),
# 作为 checkpoint 段的返回值传出, 避免用 hook 持有段内张量引用(会阻止激活释放)
moe = layer.block_sparse_moe
normed = layer.post_attention_layernorm(h)
b, t, d = normed.shape
flat = normed.reshape(-1, d)
top_idx, top_w, rlogits = moe.router(flat)
out = moe.experts(flat, top_idx, top_w).view(b, t, d)
return out, rlogits
def _aggregate(self, q, sources):
"""AttnRes 聚合: q (d,), sources list of (b,s,d) -> (b,s,d)"""
V = torch.stack(sources) # (S,b,t,d)
logits = torch.einsum("d,Sbtd->Sbt", q, _rms_norm(V))
alpha = logits.softmax(dim=0) # 源维度归一化
return torch.einsum("Sbt,Sbtd->btd", alpha, V)
# ---------- checkpoint 段(显存优化: 粒度从整轮细化到单层/单块) ----------
# 整轮一个段时, 反向重算会把 24 层(AttnRes 时还有每子层 stack 出的全部源副本 V)
# 同时留在显存里 -> OOM。细化后每段反向只重算一层/一个块。
def _layer_fwd(self, layer, h, mask, pos_emb, mu):
"""非 AttnRes 路径: 一个完整层(attn + moe), 返回 (h, router_logits)。"""
f = self._sublayer(layer, "attn", h, mask, pos_emb)
h = h + mu * f
f, rl = self._sublayer(layer, "moe", h, mask, pos_emb)
return h + mu * f, rl
def _attnres_step(self, layer, kind, q, cat, inv_rms, block_sum, mask, pos_emb, mu):
"""AttnRes 路径: 聚合 + 单个子层 + 块内累加, 返回 (新 block_sum, router_logits 或 None)。
性能优化版聚合(profile 显示旧的"每子层 stack 全部源 + 对全部源做 RMSNorm"
贡献了 ~60% 的 CUDA 时间, 全是 elementwise/cat):
- cat: (S,b,t,d) 块内固定源的堆叠, 每块只 stack 一次(块级缓存)
- inv_rms: (S,b,t) 各固定源的 RMSNorm 系数, 利用 q·RMSNorm(k) = (q·k)·inv_rms
- softmax 拆成 [固定源 | block_sum] 两段, 每子层只需对 block_sum 单张量求范数
数学上与 _aggregate 完全等价(仅浮点运算次序不同)。"""
logits = torch.einsum("d,sbtd->sbt", q, cat) * inv_rms
if block_sum is not None:
bs_inv = torch.rsqrt(block_sum.float().pow(2).mean(-1) + 1e-6)
bs_logit = torch.einsum("d,btd->bt", q, block_sum) * bs_inv
alpha = torch.cat([logits, bs_logit.unsqueeze(0)], dim=0).softmax(dim=0)
else:
alpha = logits.softmax(dim=0)
# 分析钩子: 仅分析脚本设置 alpha_recorder 时生效(训练时恒为 None, 零开销)。
# 记录 (上下文(轮,块,层序,类型), 各源平均权重), 用于检验"跨轮检索是否偷懒"。
rec = getattr(self, "alpha_recorder", None)
if rec is not None:
with torch.no_grad():
rec.append((self._rec_ctx, alpha.detach().mean(dim=(1, 2)).cpu()))
alpha = alpha.to(cat.dtype)
if block_sum is not None:
h = torch.einsum("sbt,sbtd->btd", alpha[:-1], cat) + alpha[-1].unsqueeze(-1) * block_sum
else:
h = torch.einsum("sbt,sbtd->btd", alpha, cat)
f = self._sublayer(layer, kind, h, mask, pos_emb)
rl = None
if kind == "moe":
f, rl = f
upd = mu * f
return (upd if block_sum is None else block_sum + upd), rl
def _run_block(self, blk_layers, q_blk, fixed, mask, pos_emb, mu):
"""一个 AttnRes 块(3 层 / 6 个子层), 作为一个 checkpoint 段。
源堆叠 cat 与 inv_rms 在段内构建(而非段外传入): 段内张量在前向结束后即释放,
反向重算时重建, 避免 32 个块的 cat(~0.5GB each)同时常驻显存。
profile 显示旧的"每子层 stack 全部源 + 对全部源 RMSNorm"贡献 ~60% CUDA 时间,
这里每块只 stack/归一化一次, 块内 6 个子层复用, 并用
q·RMSNorm(k) = (q·k)·inv_rms 把归一化折成标量乘法(数学上与 _aggregate 等价)。"""
cat = torch.stack(fixed) # (S,b,t,d)
inv_rms = torch.rsqrt(cat.float().pow(2).mean(-1) + 1e-6) # (S,b,t) fp32
block_sum = None
rlogits = []
for j, layer in enumerate(blk_layers):
for kind in ("attn", "moe"):
q = q_blk[2 * j] if kind == "attn" else q_blk[2 * j + 1]
self._rec_ctx = (*self._rec_ctx_base, j, kind) # 分析钩子用, 训练时无读取方
block_sum, rl = self._attnres_step(
layer, kind, q, cat, inv_rms, block_sum, mask, pos_emb, mu)
if rl is not None:
rlogits.append(rl)
return block_sum, rlogits
def _ce(self, h, labels):
"""单轮 CE, 单独成段: 避免 4 轮的 logits(fp32 上转后每轮 ~1.2GB)常驻反传图。"""
logits = self.lm_head(self.model.norm(h)) / self.logits_scaling
return self.loss_function(logits, labels, vocab_size=self.config.vocab_size)
# ---------- 一轮循环 ----------
def _run_round(self, h, round_idx, b0, prev_blocks, mask, pos_emb, mu, use_ckpt):
"""返回 (本轮最终 hidden, 块摘要列表, 各 MoE 层的 router_logits)。"""
layers = self.model.layers
rlogits = []
if not self.use_attnres:
for layer in layers:
if use_ckpt:
h, rl = checkpoint(
lambda hh, l=layer: self._layer_fwd(l, hh, mask, pos_emb, mu),
h, use_reentrant=False)
else:
h, rl = self._layer_fwd(layer, h, mask, pos_emb, mu)
rlogits.append(rl)
block_sums = []
else:
fixed = [b0] + (list(prev_blocks) if self.cross_round else [])
block_sums = []
for blk in range(self.attnres_blocks):
blk_layers = layers[blk * self.layers_per_block:(blk + 1) * self.layers_per_block]
q_blk = self.pseudo_queries[round_idx,
blk * self.sub_per_block:(blk + 1) * self.sub_per_block]
fix_snap = tuple(fixed) # fixed 之后会 append, 必须快照, 否则重算时形状对不上
self._rec_ctx_base = (round_idx, blk) # 分析钩子用
if use_ckpt:
block_sum, rls = checkpoint(
lambda _q, bl=blk_layers, qb=q_blk, fs=fix_snap:
self._run_block(bl, qb, fs, mask, pos_emb, mu),
q_blk, use_reentrant=False)
else:
block_sum, rls = self._run_block(blk_layers, q_blk, fix_snap, mask, pos_emb, mu)
rlogits.extend(rls)
fixed.append(block_sum)
block_sums.append(block_sum)
# 终读出: 对 [b0] + (跨轮时的历史块) + 本轮各块摘要 做最后一次注意力,
# 而不是只取最后一个块的部分和
final_sources = [b0] + (list(prev_blocks) if self.cross_round else []) + block_sums
q_final = self.pseudo_queries[round_idx, 2 * len(layers)]
if use_ckpt:
h = checkpoint(
lambda _q, qq=q_final, src=tuple(final_sources): self._aggregate(qq, list(src)),
q_final, use_reentrant=False)
else:
h = self._aggregate(q_final, final_sources)
return h, block_sums, rlogits
# ---------- 训练前向 ----------
def forward(self, input_ids=None, labels=None, attention_mask=None, **kwargs):
model = self.model
bsz, seq = input_ids.shape
position_ids = torch.arange(seq, device=input_ids.device).unsqueeze(0)
h = model.embed_tokens(input_ids) * model.embedding_multiplier
mask = create_causal_mask(
config=self.config, inputs_embeds=h, attention_mask=attention_mask,
past_key_values=None, position_ids=position_ids,
)
pos_emb = model.rotary_emb(h, position_ids)
mu = self.config.residual_multiplier
b0 = h
use_ckpt = self.training and torch.is_grad_enabled()
ce_losses = []
all_rlogits = []
prev_blocks = [] # cross_round 时跨轮累积的块摘要
for r in range(self.loop_rounds):
h, round_blocks, rlogits = self._run_round(
h, r, b0, prev_blocks, mask, pos_emb, mu, use_ckpt)
if self.round_recorder is not None:
self.round_recorder.append(h.detach())
if self.cross_round:
prev_blocks = prev_blocks + round_blocks
if labels is not None:
if use_ckpt:
ce = checkpoint(lambda hh=h, lb=labels: self._ce(hh, lb),
h, use_reentrant=False)
else:
ce = self._ce(h, labels)
ce_losses.append(ce)
all_rlogits.extend(rlogits)
loss = None
aux = None
round_losses = None
logits = None
if labels is None:
# 评测路径: 返回最终轮的 logits (训练路径 labels 恒非 None, 不受影响)
logits = self.lm_head(self.model.norm(h)) / self.logits_scaling
if labels is not None:
loss = torch.stack(ce_losses).mean()
round_losses = [c.detach() for c in ce_losses] # 监控用: 各轮 CE 分开看
if all_rlogits:
aux = load_balancing_loss_func(
tuple(all_rlogits), self.num_experts, self.num_experts_per_tok)
loss = loss + self.router_aux_loss_coef * aux
return SimpleNamespace(loss=loss, logits=logits, aux_loss=aux, round_losses=round_losses)
# ---------- 推理: KV cache 贪心生成 ----------
# 思路: 循环架构下, 过去位置在第 r 轮第 l 层的注意力输入是静态的(因果性保证
# 之后不再改变), 因此可以按 (round, layer) 缓存 K/V。预填充阶段复用标准前向,
# 用 pre-hook 捕获每个注意力子层的输入并现算 K/V; 解码阶段每 token 只算当前
# 位置, 数学上与整序列重算等价(仅浮点运算次序差异)。
@torch.no_grad()
def _prefill_kv(self, input_ids):
"""跑一遍标准前向, 返回 (最后一位的 logits, cache[r][l]=(K,V))。"""
captured = []
hooks = []
for layer in self.model.layers:
hooks.append(layer.self_attn.register_forward_pre_hook(
lambda m, args, kwargs: captured.append(kwargs["hidden_states"].detach()),
with_kwargs=True))
try:
out = self.forward(input_ids=input_ids)
finally:
for hk in hooks:
hk.remove()
L = len(self.model.layers)
assert len(captured) == self.loop_rounds * L, \
f"hook 捕获数 {len(captured)} != 轮数×层数 {self.loop_rounds * L}"
from transformers.models.granitemoe.modeling_granitemoe import apply_rotary_pos_emb
bsz, seq = input_ids.shape
pos_ids = torch.arange(seq, device=input_ids.device).unsqueeze(0)
dummy = torch.zeros(1, seq, self.config.hidden_size, device=input_ids.device,
dtype=self.model.embed_tokens.weight.dtype)
cos, sin = self.model.rotary_emb(dummy, pos_ids)
cache = []
for r in range(self.loop_rounds):
rc = []
for l, layer in enumerate(self.model.layers):
x = captured[r * L + l]
sa = layer.self_attn
hd, kvh = sa.head_dim, self.config.num_key_value_heads
k = sa.k_proj(x).view(bsz, seq, kvh, hd).transpose(1, 2)
v = sa.v_proj(x).view(bsz, seq, kvh, hd).transpose(1, 2)
_, k = apply_rotary_pos_emb(
torch.zeros(bsz, self.config.num_attention_heads, seq, hd,
device=x.device, dtype=k.dtype), k, cos, sin)
rc.append((k, v))
cache.append(rc)
return out.logits[:, -1], cache
def _attn_decode(self, layer, x, k_cache, v_cache, cos, sin):
"""解码阶段的单 token 注意力: x (b,1,d) 已加 cache 的 K/V (b,kvh,t,hd)。"""
from transformers.models.granitemoe.modeling_granitemoe import apply_rotary_pos_emb
sa = layer.self_attn
b = x.shape[0]
nh, kvh, hd = self.config.num_attention_heads, self.config.num_key_value_heads, sa.head_dim
q = sa.q_proj(x).view(b, 1, nh, hd).transpose(1, 2)
k = sa.k_proj(x).view(b, 1, kvh, hd).transpose(1, 2)
v = sa.v_proj(x).view(b, 1, kvh, hd).transpose(1, 2)
q, k = apply_rotary_pos_emb(q, k, cos, sin)
K = torch.cat([k_cache, k], dim=2)
V = torch.cat([v_cache, v], dim=2)
g = nh // kvh
Kf = K.repeat_interleave(g, dim=1)
Vf = V.repeat_interleave(g, dim=1)
w = (q @ Kf.transpose(-1, -2)) * sa.scaling
o = w.softmax(-1) @ Vf # (b,nh,1,hd)
o = o.transpose(1, 2).reshape(b, 1, nh * hd)
return sa.o_proj(o), K, V
@torch.no_grad()
def greedy_generate(self, input_ids, max_new_tokens=64, eos_token_id=None):
"""KV cache 贪心生成, 返回生成的 id 列表(不含 prompt)。仅支持 batch=1。"""
assert input_ids.shape[0] == 1
dev = input_ids.device
layers = self.model.layers
mu = self.config.residual_multiplier
L = len(layers)
logits, cache = self._prefill_kv(input_ids)
nxt = logits[0].argmax().item()
out_ids = []
pos = input_ids.shape[1]
for _ in range(max_new_tokens):
if eos_token_id is not None and nxt == eos_token_id:
break
out_ids.append(nxt)
pos_ids = torch.tensor([[pos]], device=dev)
cos, sin = self.model.rotary_emb(
torch.zeros(1, 1, self.config.hidden_size, device=dev,
dtype=self.model.embed_tokens.weight.dtype), pos_ids)
h = self.model.embed_tokens(
torch.tensor([[nxt]], device=dev)) * self.model.embedding_multiplier
b0 = h
prev_blocks = []
for r in range(self.loop_rounds):
if not self.use_attnres:
for l, layer in enumerate(layers):
f, K, V = self._attn_decode(
layer, layer.input_layernorm(h), cache[r][l][0], cache[r][l][1], cos, sin)
cache[r][l] = (K, V)
h = h + mu * f
f, _ = self._sublayer(layer, "moe", h, None, None)
h = h + mu * f
else:
fixed = [b0] + (list(prev_blocks) if self.cross_round else [])
block_sums = []
for blk in range(self.attnres_blocks):
blk_layers = layers[blk * self.layers_per_block:(blk + 1) * self.layers_per_block]
q_blk = self.pseudo_queries[r,
blk * self.sub_per_block:(blk + 1) * self.sub_per_block]
cat = torch.stack(fixed)
inv_rms = torch.rsqrt(cat.float().pow(2).mean(-1) + 1e-6)
block_sum = None
for j, layer in enumerate(blk_layers):
for kind in ("attn", "moe"):
q = q_blk[2 * j] if kind == "attn" else q_blk[2 * j + 1]
block_sum, _ = self._attnres_step_decode(
layer, kind, q, cat, inv_rms, block_sum, cos, sin, mu,
cache, r, blk * self.layers_per_block + j)
fixed.append(block_sum)
block_sums.append(block_sum)
final_sources = [b0] + (list(prev_blocks) if self.cross_round else []) + block_sums
q_final = self.pseudo_queries[r, 2 * L]
h = self._aggregate(q_final, final_sources)
if self.cross_round:
prev_blocks = prev_blocks + block_sums
logits = self.lm_head(self.model.norm(h)) / self.logits_scaling
nxt = logits[0, -1].argmax().item()
pos += 1
return out_ids
def _attnres_step_decode(self, layer, kind, q, cat, inv_rms, block_sum, cos, sin, mu,
cache, r, l):
"""_attnres_step 的解码版: 聚合 + 单个子层(单 token, 注意力走 cache)。"""
logits = torch.einsum("d,sbtd->sbt", q, cat) * inv_rms
if block_sum is not None:
bs_inv = torch.rsqrt(block_sum.float().pow(2).mean(-1) + 1e-6)
bs_logit = torch.einsum("d,btd->bt", q, block_sum) * bs_inv
alpha = torch.cat([logits, bs_logit.unsqueeze(0)], dim=0).softmax(dim=0)
else:
alpha = logits.softmax(dim=0)
alpha = alpha.to(cat.dtype)
if block_sum is not None:
h = torch.einsum("sbt,sbtd->btd", alpha[:-1], cat) + alpha[-1].unsqueeze(-1) * block_sum
else:
h = torch.einsum("sbt,sbtd->btd", alpha, cat)
if kind == "attn":
f, K, V = self._attn_decode(
layer, layer.input_layernorm(h), cache[r][l][0], cache[r][l][1], cos, sin)
cache[r][l] = (K, V)
rl = None
else:
f, rl = self._sublayer(layer, "moe", h, None, None)
upd = mu * f
return (upd if block_sum is None else block_sum + upd), rl